Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ca0df18bc | ||
|
|
ffeb7fe4f3 | ||
|
|
07f5ea0dbe | ||
|
|
ee6dc40393 | ||
|
|
cd1f2e76f4 | ||
|
|
a70d1d09bb | ||
|
|
b034c3f09f | ||
|
|
2583c237f2 | ||
|
|
c83958c457 | ||
|
|
a0b2d99bc7 | ||
|
|
ba145c4722 | ||
|
|
c21dbfe1e5 | ||
|
|
fc38b5c998 | ||
|
|
d26fcf6fc8 |
@@ -16,12 +16,84 @@ jobs:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.repository_owner == 'Reithan' }}
|
||||
env:
|
||||
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
fetch-depth: 2 # Need HEAD and HEAD~1 for version comparison
|
||||
- name: Check version increment
|
||||
id: version_check
|
||||
run: |
|
||||
python3 << 'EOF'
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
from packaging.version import parse as parse_version
|
||||
|
||||
def extract_version(file_content):
|
||||
"""Extract version string from pyproject.toml content."""
|
||||
for line in file_content.split('\n'):
|
||||
if line.strip().startswith('version = '):
|
||||
version_str = line.split('=', 1)[1].strip()
|
||||
return version_str.strip('"').strip("'")
|
||||
return None
|
||||
|
||||
def get_version_at_commit(commit_ref):
|
||||
"""Get version from pyproject.toml at a specific commit."""
|
||||
try:
|
||||
result = subprocess.run(
|
||||
['git', 'show', f'{commit_ref}:pyproject.toml'],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True
|
||||
)
|
||||
return extract_version(result.stdout)
|
||||
except subprocess.CalledProcessError:
|
||||
return None
|
||||
|
||||
# Get current and previous versions
|
||||
current_version_str = get_version_at_commit('HEAD')
|
||||
if not current_version_str:
|
||||
print("::error::Could not extract current version from pyproject.toml")
|
||||
sys.exit(1)
|
||||
|
||||
previous_version_str = get_version_at_commit('HEAD~1')
|
||||
|
||||
# Handle first commit case
|
||||
if not previous_version_str:
|
||||
print(f"::notice::First version commit detected: {current_version_str}")
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write("should_publish=true\n")
|
||||
sys.exit(0)
|
||||
|
||||
# Parse and compare versions
|
||||
try:
|
||||
current_version = parse_version(current_version_str)
|
||||
previous_version = parse_version(previous_version_str)
|
||||
except Exception as e:
|
||||
print(f"::error::Invalid version format - {e}")
|
||||
print(f"Current: {current_version_str}, Previous: {previous_version_str}")
|
||||
sys.exit(1)
|
||||
|
||||
# Version comparison logic
|
||||
if current_version > previous_version:
|
||||
print(f"::notice::Version increment detected: {previous_version_str} -> {current_version_str}")
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write("should_publish=true\n")
|
||||
elif current_version == previous_version:
|
||||
print(f"::notice::Version unchanged: {current_version_str} - skipping publish")
|
||||
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
|
||||
f.write("should_publish=false\n")
|
||||
else:
|
||||
print(f"::error::Version downgrade detected: {previous_version_str} -> {current_version_str}")
|
||||
print("::error::Version must increase. Downgrades would conflict with published versions.")
|
||||
sys.exit(1)
|
||||
EOF
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
if: steps.version_check.outputs.should_publish == 'true'
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
with:
|
||||
personal_access_token: ${{ secrets.COMFY_REGISTRY_KEY }}
|
||||
|
||||
@@ -170,5 +170,13 @@ cython_debug/
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# pre-commit stuff:
|
||||
.pre-commit-cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
|
||||
# AI agents
|
||||
.claude/settings.local.json
|
||||
.claude/docs/**
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# See https://pre-commit.com for more information
|
||||
repos:
|
||||
# Standard pre-commit hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: check-added-large-files
|
||||
args: ['--maxkb=1000']
|
||||
- id: check-merge-conflict
|
||||
- id: mixed-line-ending
|
||||
args: ['--fix=lf']
|
||||
|
||||
# Ruff linter and formatter
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
rev: v0.15.12
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [--fix, --exit-non-zero-on-fix]
|
||||
- id: ruff-format
|
||||
|
||||
# Main branch protection (pre-commit)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: prevent-commit-to-main
|
||||
name: Prevent commits to main branch
|
||||
entry: bash -c 'BRANCH=$(git branch --show-current); if [ "$BRANCH" = "main" ]; then echo "ERROR - Direct commits to main are not allowed. Create a feature branch instead."; exit 1; fi'
|
||||
language: system
|
||||
stages: [pre-commit]
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
# Test execution (pre-push only)
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: run-tests
|
||||
name: Run pytest tests
|
||||
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'
|
||||
language: system
|
||||
stages: [pre-push]
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
|
||||
- id: prevent-push-to-main
|
||||
name: Prevent pushes to main branch
|
||||
entry: bash -c 'BRANCH=$(git branch --show-current); if [ "$BRANCH" = "main" ]; then echo "ERROR - Direct pushes to main are not allowed. Use pull requests instead."; exit 1; fi'
|
||||
language: system
|
||||
stages: [pre-push]
|
||||
always_run: true
|
||||
pass_filenames: false
|
||||
+260
@@ -0,0 +1,260 @@
|
||||
# Contributing to Negative Rejection Steering
|
||||
|
||||
Thank you for your interest in contributing! This document provides guidelines for setting up your development environment and contributing to the project.
|
||||
|
||||
## Development Setup
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.10 or higher
|
||||
- [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager
|
||||
- Git
|
||||
|
||||
### Initial Setup
|
||||
|
||||
1. **Clone the repository** (if you haven't already):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Reithan/negative_rejection_steering.git
|
||||
cd negative_rejection_steering
|
||||
```
|
||||
|
||||
2. **Create virtual environment with uv**:
|
||||
|
||||
```bash
|
||||
uv venv
|
||||
```
|
||||
|
||||
3. **Activate virtual environment**:
|
||||
|
||||
```bash
|
||||
# Git Bash (Windows)
|
||||
source .venv/Scripts/activate
|
||||
|
||||
# Linux/Mac
|
||||
source .venv/bin/activate
|
||||
|
||||
# Windows CMD
|
||||
.venv\Scripts\activate.bat
|
||||
|
||||
# Windows PowerShell
|
||||
.venv\Scripts\Activate.ps1
|
||||
```
|
||||
|
||||
4. **Install development dependencies**:
|
||||
|
||||
```bash
|
||||
uv pip install -e ".[dev]"
|
||||
```
|
||||
|
||||
5. **Install git hooks**:
|
||||
|
||||
```bash
|
||||
pre-commit install
|
||||
pre-commit install --hook-type pre-push
|
||||
```
|
||||
|
||||
6. **Verify setup**:
|
||||
|
||||
```bash
|
||||
# Run hooks manually on all files
|
||||
pre-commit run --all-files
|
||||
|
||||
# Check that ruff works
|
||||
ruff check .
|
||||
```
|
||||
|
||||
## Git Workflow
|
||||
|
||||
### Protected Branches
|
||||
|
||||
- **Direct commits to `main` are blocked** by git hooks
|
||||
- **Direct pushes to `main` are blocked** by git hooks
|
||||
- All changes must go through feature branches and pull requests
|
||||
|
||||
### Recommended Workflow
|
||||
|
||||
1. **Create a feature branch**:
|
||||
|
||||
```bash
|
||||
git checkout -b feature/your-feature-name
|
||||
```
|
||||
|
||||
Or for bug fixes:
|
||||
|
||||
```bash
|
||||
git checkout -b fix/bug-description
|
||||
```
|
||||
|
||||
2. **Make your changes and commit**:
|
||||
|
||||
```bash
|
||||
git add <files>
|
||||
git commit -m "Your commit message"
|
||||
```
|
||||
|
||||
The pre-commit hook will automatically:
|
||||
- Run ruff linting and auto-fix issues
|
||||
- Check for trailing whitespace, missing final newlines, etc.
|
||||
- Block the commit if you're on the main branch
|
||||
|
||||
If the linter auto-fixes files, you'll need to re-stage and commit again.
|
||||
|
||||
3. **Push your branch**:
|
||||
|
||||
```bash
|
||||
git push origin feature/your-feature-name
|
||||
```
|
||||
|
||||
The pre-push hook will:
|
||||
- Run tests (if pytest is available)
|
||||
- Block the push if you're on the main branch
|
||||
|
||||
4. **Create a pull request** on GitHub
|
||||
|
||||
5. **Merge after review**
|
||||
|
||||
### Bypassing Hooks (Emergency Only)
|
||||
|
||||
If you must bypass hooks (NOT recommended):
|
||||
|
||||
```bash
|
||||
git commit --no-verify # Skip pre-commit hooks
|
||||
git push --no-verify # Skip pre-push hooks
|
||||
```
|
||||
|
||||
**Warning**: Only use `--no-verify` in emergencies. Bypassing hooks may:
|
||||
|
||||
- Introduce linting issues
|
||||
- Break Continuous Integration/Continuous Deployment pipelines
|
||||
- Allow untested code to be pushed
|
||||
|
||||
## Development Commands
|
||||
|
||||
### Linting
|
||||
|
||||
```bash
|
||||
# Check for linting issues
|
||||
ruff check .
|
||||
|
||||
# Auto-fix linting issues
|
||||
ruff check --fix .
|
||||
|
||||
# Format code
|
||||
ruff format .
|
||||
|
||||
# Check a specific file
|
||||
ruff check NRS/nodes_NRS.py
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pytest
|
||||
|
||||
# Run with verbose output
|
||||
pytest -v
|
||||
|
||||
# Run specific test file
|
||||
pytest tests/test_smoke.py
|
||||
|
||||
# Run specific test function
|
||||
pytest tests/test_smoke.py::test_file_structure
|
||||
```
|
||||
|
||||
**Note**: Tests use mocked versions of torch and gradio (via conftest.py) since these dependencies are provided by ComfyUI/WebUI at runtime.
|
||||
|
||||
### Pre-commit Hooks
|
||||
|
||||
```bash
|
||||
# Run all hooks manually
|
||||
pre-commit run --all-files
|
||||
|
||||
# Run specific hook
|
||||
pre-commit run ruff --all-files
|
||||
pre-commit run ruff-format --all-files
|
||||
|
||||
# Update hook versions
|
||||
pre-commit autoupdate
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
This project uses **ruff** for linting and formatting with the following configuration:
|
||||
|
||||
- **Line length**: 120 characters
|
||||
- **Target Python version**: 3.10+
|
||||
- **Enabled checks**: pycodestyle (E/W), pyflakes (F), isort (I), pep8-naming (N), pyupgrade (UP)
|
||||
|
||||
### Special Cases
|
||||
|
||||
- **ComfyUI API conventions**: The `INPUT_TYPES` method and `s` parameter naming are required by ComfyUI's API and are exempted from normal naming rules
|
||||
- **Star imports in `__init__.py`**: Required for ComfyUI node discovery
|
||||
|
||||
## Commit Message Guidelines
|
||||
|
||||
Write clear, concise commit messages:
|
||||
|
||||
- Use imperative mood ("Add feature" not "Added feature")
|
||||
- Keep first line under 72 characters
|
||||
- Add detailed description in the body if needed
|
||||
|
||||
Good examples:
|
||||
|
||||
```
|
||||
Add support for XYZ model type
|
||||
Fix crash when prediction type is unknown
|
||||
Update README with installation instructions
|
||||
```
|
||||
|
||||
Bad examples:
|
||||
|
||||
```
|
||||
fixed stuff
|
||||
WIP
|
||||
Updated code
|
||||
```
|
||||
|
||||
## Pull Request Guidelines
|
||||
|
||||
When submitting a pull request:
|
||||
|
||||
1. **Keep PRs focused**: One feature or fix per PR
|
||||
2. **Update documentation**: If you add features, update README.md
|
||||
3. **Test your changes**: Ensure the extension works in ComfyUI/reForge
|
||||
4. **Run pre-commit hooks**: Make sure all checks pass
|
||||
5. **Describe your changes**: Explain what and why in the PR description
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
negative_rejection_steering/
|
||||
├── NRS/
|
||||
│ └── nodes_NRS.py # Main NRS node implementation
|
||||
├── scripts/
|
||||
│ └── negative_rejection_steering_script.py # Gradio UI for reForge
|
||||
├── tests/
|
||||
│ └── test_smoke.py # Smoke tests
|
||||
├── __init__.py # ComfyUI node exports
|
||||
├── pyproject.toml # Project config, dependencies, tool config
|
||||
├── .pre-commit-config.yaml # Git hooks configuration
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
├── LICENSE
|
||||
└── CONTRIBUTING.md # This file
|
||||
```
|
||||
|
||||
## Getting Help
|
||||
|
||||
- **Issues**: Report bugs or request features via [GitHub Issues](https://github.com/Reithan/negative_rejection_steering/issues)
|
||||
- **Discussions**: For questions or general discussion
|
||||
- **Pull Requests**: Review the PR guidelines above before submitting
|
||||
|
||||
## License
|
||||
|
||||
By contributing to this project, you agree that your contributions will be licensed under the same license as the project (see LICENSE file).
|
||||
|
||||
---
|
||||
|
||||
Thank you for contributing to Negative Rejection Steering!
|
||||
+141
-100
@@ -1,37 +1,79 @@
|
||||
import logging
|
||||
import torch
|
||||
from enum import Enum, auto
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
|
||||
# fmt: off
|
||||
class PredictionType(Enum):
|
||||
EPS = auto() # ε-prediction
|
||||
V = auto() # v-prediction
|
||||
X0 = auto() # x₀-prediction
|
||||
UNKNOWN = auto() # couldn’t detect / new scheduler
|
||||
|
||||
|
||||
_RAW_TO_ENUM = {
|
||||
"eps": PredictionType.EPS,
|
||||
"epsilon": PredictionType.EPS,
|
||||
"flux": PredictionType.EPS,
|
||||
"chroma": PredictionType.EPS,
|
||||
"flow": PredictionType.EPS, # FLOW models (WAN, etc.) are EPS-compatible
|
||||
"wan": PredictionType.EPS, # WAN21 is FLOW-based
|
||||
"const": PredictionType.EPS, # CONST prediction class used in FLOW models
|
||||
"v": PredictionType.V,
|
||||
"v_prediction": PredictionType.V,
|
||||
"x0": PredictionType.X0,
|
||||
"sample": PredictionType.X0,
|
||||
}
|
||||
# fmt: on
|
||||
|
||||
|
||||
class NRS:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {"required": { "model": ("MODEL",),
|
||||
"skew": ("FLOAT", {"default": 4.0, "min": -30.0, "max": 30.0, "step": 0.01}),
|
||||
"stretch": ("FLOAT", {"default": 2.0, "min": -30.0, "max": 30.0, "step": 0.01}),
|
||||
"squash": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 1.0, "step": 0.01}),
|
||||
}}
|
||||
return {
|
||||
"required": {
|
||||
"model": ("MODEL", {"tooltip": "Input model to apply NRS to"}),
|
||||
"skew": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 2.00,
|
||||
"min": -30.0,
|
||||
"max": 30.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "Changes the 'direction' of generation, steering away from negative prompt elements. Start with CFG/2.",
|
||||
},
|
||||
),
|
||||
"stretch": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 5.00,
|
||||
"min": -30.0,
|
||||
"max": 30.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "Intensifies positive prompt elements. Start with your normal CFG value.",
|
||||
},
|
||||
),
|
||||
"squash": (
|
||||
"FLOAT",
|
||||
{
|
||||
"default": 0.75,
|
||||
"min": 0.0,
|
||||
"max": 1.0,
|
||||
"step": 0.01,
|
||||
"tooltip": "Softens Skew/Stretch effects, adding micro-detailing. Keep low initially.",
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("MODEL",)
|
||||
FUNCTION = "patch"
|
||||
|
||||
CATEGORY = "advanced/model"
|
||||
|
||||
DESCRIPTION = "Negative Rejection Steering (NRS) replaces CFG with more nuanced guidance. IMPORTANT: Set your KSampler CFG to any value (it will be ignored). Connect your model through this node before sampling."
|
||||
|
||||
def _get_pred_type(self, model) -> PredictionType:
|
||||
"""
|
||||
In order to support Comfy, Forge, and possibly other models
|
||||
@@ -40,6 +82,7 @@ class NRS:
|
||||
prediction-type flag, then map it to the enum.
|
||||
Defaults to EPS if all else fails.
|
||||
"""
|
||||
|
||||
def _canon(p):
|
||||
if p is None:
|
||||
return ""
|
||||
@@ -59,7 +102,12 @@ class NRS:
|
||||
for attr in ("model_type", "prediction_type", "parameterization"):
|
||||
p = _canon(getattr(obj, attr, None))
|
||||
if p:
|
||||
return _RAW_TO_ENUM.get(p, PredictionType.UNKNOWN)
|
||||
pred_type = _RAW_TO_ENUM.get(p, PredictionType.UNKNOWN)
|
||||
if pred_type != PredictionType.UNKNOWN:
|
||||
logging.debug(
|
||||
f"NRS._get_pred_type: Found prediction type '{p}' from attribute '{attr}' -> {pred_type}"
|
||||
)
|
||||
return pred_type
|
||||
|
||||
# 2) enqueue child containers we care about -------------------
|
||||
for attr in ("model", "diffusion_model", "config", "scheduler", "inner_model", "model_sampling"):
|
||||
@@ -68,123 +116,125 @@ class NRS:
|
||||
seen.add(id(child))
|
||||
queue.append(child)
|
||||
|
||||
# 3) default ------------------------------------------------------
|
||||
return PredictionType.UNKNOWN
|
||||
|
||||
def _convert_to_eps_space(self, x_orig, sig_root, sigma, cond, uncond):
|
||||
x_div = None
|
||||
eps_cond = cond
|
||||
eps_uncond = uncond
|
||||
if self.__pred_type == PredictionType.V:
|
||||
# v → ε conversion
|
||||
logging.debug(f"NRS._convert_to_eps_space: generating x_div, eps_cond, and eps_uncond for v-pred")
|
||||
x_div = x_orig / (sigma ** 2 + 1)
|
||||
# 3) enhanced detection for FLOW models (WAN, Flux, etc.) -------
|
||||
try:
|
||||
# Check model_sampling class type for FLOW models
|
||||
if hasattr(model, "model_sampling") and model.model_sampling is not None:
|
||||
sampling_class_name = type(model.model_sampling).__name__.lower()
|
||||
logging.debug(f"NRS._get_pred_type: Found model_sampling class: {sampling_class_name}")
|
||||
|
||||
eps_cond = ((x_div - (x_orig - cond)) * sig_root) / (sigma)
|
||||
eps_uncond = ((x_div - (x_orig - uncond)) * sig_root) / (sigma)
|
||||
elif self.__pred_type == PredictionType.EPS:
|
||||
logging.debug(f"NRS._convert_to_eps_space: already in eps, no pre-scale needed")
|
||||
pass # already in ε space
|
||||
elif self.__pred_type == PredictionType.X0:
|
||||
raise NotImplementedError("NRS._convert_to_eps_space: x0-prediction not supported yet.")
|
||||
else:
|
||||
raise RuntimeError("NRS._convert_to_eps_space: Could not determine prediction type for this model.")
|
||||
|
||||
return x_div, eps_cond, eps_uncond
|
||||
# CONST class is used by FLOW models (WAN21, Flux, etc.)
|
||||
if "const" in sampling_class_name:
|
||||
logging.debug("NRS._get_pred_type: Detected FLOW model via CONST sampling class -> EPS")
|
||||
return PredictionType.EPS
|
||||
elif "v_prediction" in sampling_class_name:
|
||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via sampling class -> V")
|
||||
return PredictionType.V
|
||||
elif "eps" in sampling_class_name:
|
||||
logging.debug("NRS._get_pred_type: Detected EPS model via sampling class -> EPS")
|
||||
return PredictionType.EPS
|
||||
|
||||
def _finalize_from_eps_space(self, x_orig, x_div, x_final, sig_root, sigma):
|
||||
nrs_result = x_final
|
||||
if self.__pred_type == PredictionType.V:
|
||||
# ε → v conversion
|
||||
logging.debug(f"NRS._finalize_from_eps_space: generating cfg_result for v-pred")
|
||||
nrs_result = x_orig - (x_div - x_final * sigma / sig_root)
|
||||
elif self.__pred_type == PredictionType.EPS:
|
||||
# already in ε space
|
||||
logging.debug(f"NRS._finalize_from_eps_space: already in eps, no post-scale needed")
|
||||
pass
|
||||
elif self.__pred_type == PredictionType.X0:
|
||||
raise NotImplementedError("NRS._finalize_from_eps_space: x0-prediction not supported yet.")
|
||||
else:
|
||||
raise RuntimeError("NRS._finalize_from_eps_space: Could not determine prediction type for this model.")
|
||||
return nrs_result
|
||||
|
||||
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond):
|
||||
# Check model.model.model_type enum for newer models
|
||||
if hasattr(model, "model") and hasattr(model.model, "model_type"):
|
||||
model_type_str = _canon(str(model.model.model_type))
|
||||
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:
|
||||
logging.debug("NRS._get_pred_type: Detected FLOW/Flux model via model_type -> EPS")
|
||||
return PredictionType.EPS
|
||||
elif "v_prediction" in model_type_str:
|
||||
logging.debug("NRS._get_pred_type: Detected V-prediction model via model_type -> V")
|
||||
return PredictionType.V
|
||||
elif "eps" in model_type_str:
|
||||
logging.debug("NRS._get_pred_type: Detected EPS model via model_type -> EPS")
|
||||
return PredictionType.EPS
|
||||
|
||||
except Exception as e:
|
||||
logging.debug(f"NRS._get_pred_type: Exception during enhanced detection: {e}")
|
||||
|
||||
# 4) safe default (matches docstring promise) --------------------
|
||||
logging.warning("NRS._get_pred_type: Could not determine prediction type for model. Using EPS as fallback.")
|
||||
logging.debug(
|
||||
f"NRS._get_pred_type: Model structure: {[attr for attr in dir(model) if not attr.startswith('_')]}"
|
||||
)
|
||||
return PredictionType.EPS
|
||||
|
||||
def _convert_to_v_space(self, x_orig, sig_root, sigma, cond, uncond, pred_type):
|
||||
x_div = None
|
||||
v_cond = cond
|
||||
v_uncond = uncond
|
||||
if self.__pred_type == PredictionType.V:
|
||||
logging.debug(f"NRS._convert_to_v_space: already in v, no pre-scale needed")
|
||||
if pred_type == PredictionType.V:
|
||||
logging.debug("NRS._convert_to_v_space: already in v, no pre-scale needed")
|
||||
pass # already in v space
|
||||
elif self.__pred_type == PredictionType.EPS:
|
||||
elif pred_type == PredictionType.EPS:
|
||||
# ε → v conversion
|
||||
logging.debug(f"NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps")
|
||||
x_div = x_orig / (sigma ** 2 + 1)
|
||||
logging.debug("NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps")
|
||||
x_div = x_orig / (sigma**2 + 1)
|
||||
factor = sigma / sig_root
|
||||
|
||||
v_cond = x_orig - (x_div - cond * factor)
|
||||
v_uncond = x_orig - (x_div - uncond * factor)
|
||||
elif self.__pred_type == PredictionType.X0:
|
||||
elif pred_type == PredictionType.X0:
|
||||
raise NotImplementedError("NRS._convert_to_v_space: x0-prediction not supported yet.")
|
||||
else:
|
||||
raise RuntimeError("NRS._convert_to_v_space: Could not determine prediction type for this model.")
|
||||
|
||||
# Fallback: treat UNKNOWN as EPS and convert to V-space
|
||||
logging.warning(f"NRS._convert_to_v_space: Unknown prediction type {pred_type}, treating as EPS")
|
||||
logging.debug("NRS._convert_to_v_space: generating x_div, v_cond, and v_uncond for eps (fallback)")
|
||||
x_div = x_orig / (sigma**2 + 1)
|
||||
factor = sigma / sig_root
|
||||
v_cond = x_orig - (x_div - cond * factor)
|
||||
v_uncond = x_orig - (x_div - uncond * factor)
|
||||
|
||||
return x_div, v_cond, v_uncond
|
||||
|
||||
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma):
|
||||
def _finalize_from_v_space(self, x_orig, x_div, x_final, sig_root, sigma, pred_type):
|
||||
nrs_result = x_final
|
||||
if self.__pred_type == PredictionType.V:
|
||||
if pred_type == PredictionType.V:
|
||||
# already in v space
|
||||
logging.debug(f"NRS._finalize_from_v_space: already in v, no post-scale needed")
|
||||
logging.debug("NRS._finalize_from_v_space: already in v, no post-scale needed")
|
||||
pass
|
||||
elif self.__pred_type == PredictionType.EPS:
|
||||
elif pred_type == PredictionType.EPS:
|
||||
# v → ε conversion
|
||||
logging.debug(f"NRS._finalize_from_v_space: generating cfg_result for eps")
|
||||
logging.debug("NRS._finalize_from_v_space: generating cfg_result for eps")
|
||||
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
||||
elif self.__pred_type == PredictionType.X0:
|
||||
elif pred_type == PredictionType.X0:
|
||||
raise NotImplementedError("NRS._finalize_from_v_space: x0-prediction not supported yet.")
|
||||
else:
|
||||
raise RuntimeError("NRS._finalize_from_v_space: Could not determine prediction type for this model.")
|
||||
# Fallback: treat UNKNOWN as EPS and convert from V-space
|
||||
logging.warning(f"NRS._finalize_from_v_space: Unknown prediction type {pred_type}, treating as EPS")
|
||||
logging.debug("NRS._finalize_from_v_space: generating cfg_result for eps (fallback)")
|
||||
nrs_result = (x_div - (x_orig - x_final)) * (sig_root / sigma)
|
||||
return nrs_result
|
||||
|
||||
|
||||
def patch(self, model, skew, stretch, squash):
|
||||
self.__pred_type = self._get_pred_type(model) if not hasattr(self, "__pred_type") else self.__pred_type
|
||||
self.__OPERATION_SPACE = PredictionType.V
|
||||
pred_type = self._get_pred_type(model)
|
||||
|
||||
def nrs(args):
|
||||
logging.debug(f"NRS.nrs: Skew: {skew}, Stretch: {stretch}, Squash: {squash}")
|
||||
# self.__pred_type = self.__pred_type if self.__pred_type is not None else self._get_pred_type(model)
|
||||
cond = args["cond"]
|
||||
uncond = args["uncond"]
|
||||
x_orig = args["input"]
|
||||
|
||||
|
||||
sigma = args["sigma"]
|
||||
sigma = sigma.view(sigma.shape[:1] + (1,) * (cond.ndim - 1))
|
||||
sig_root = (sigma ** 2 + 1).sqrt()
|
||||
|
||||
x_div, nrs_cond, nrs_uncond = None, None, None
|
||||
match self.__OPERATION_SPACE:
|
||||
case PredictionType.V:
|
||||
x_div, nrs_cond, nrs_uncond = self._convert_to_v_space(x_orig, sig_root, sigma, cond, uncond)
|
||||
case PredictionType.EPS:
|
||||
x_div, nrs_cond, nrs_uncond = self._convert_to_eps_space(x_orig, sig_root, sigma, cond, uncond)
|
||||
case PredictionType.X0:
|
||||
raise RuntimeError("NRS.nrs: x0-prediction not supported yet.")
|
||||
case PredictionType.UNKNOWN:
|
||||
raise RuntimeError("NRS.nrs: Could not determine prediction type for this operation.")
|
||||
case _:
|
||||
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
|
||||
sig_root = (sigma**2 + 1).sqrt()
|
||||
|
||||
# Operation space is hardcoded to V for now; FLOW is added in a later PR.
|
||||
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]
|
||||
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]
|
||||
|
||||
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)
|
||||
@@ -200,22 +250,13 @@ class NRS:
|
||||
squash_scale = (1 - squash) + (squash * (cond_len / nrs_len))
|
||||
x_final = skewed * squash_scale
|
||||
|
||||
match self.__OPERATION_SPACE:
|
||||
case PredictionType.V:
|
||||
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma)
|
||||
case PredictionType.EPS:
|
||||
return self._finalize_from_eps_space(x_orig, x_div, x_final, sig_root, sigma)
|
||||
case PredictionType.X0:
|
||||
raise RuntimeError("NRS.nrs: x0-prediction not supported yet.")
|
||||
case PredictionType.UNKNOWN:
|
||||
raise RuntimeError("NRS.nrs: Could not determine prediction type for this operation.")
|
||||
case _:
|
||||
raise RuntimeError("NRS.nrs: Invalid PredictionType used.")
|
||||
|
||||
return self._finalize_from_v_space(x_orig, x_div, x_final, sig_root, sigma, pred_type)
|
||||
|
||||
m = model.clone()
|
||||
m.set_model_sampler_cfg_function(nrs, True)
|
||||
return (m, )
|
||||
return (m,)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"NRS": NRS,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,8 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
|
||||
2. NRS replaces CFG with 3 new knobs.
|
||||
3. NRS lets you to create cooler outputs than CFG.
|
||||
|
||||
**Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
||||
|
||||
> [!TIP]
|
||||
> Skip to the [Beginner How-To](#beginner-how-to) if you want to just get started.
|
||||
|
||||
@@ -39,7 +41,7 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
|
||||
|
||||
| Prompt | |
|
||||
| ---------- | --- |
|
||||
| Tool | [Stable Diffusion WebUI reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge) |
|
||||
| Tool | [Stable Diffusion WebUI reForge](https://github.com/Panchovix/stable-diffusion-webui-reForge) |
|
||||
| Sampler | DPM++ 2M |
|
||||
| Scheduler | Align Your Steps |
|
||||
| Steps | 25 |
|
||||
@@ -60,19 +62,63 @@ NRS seeks to replace the 'naive' linear interpolation of Classifier Free Guidanc
|
||||
**Squash** is the speed limit. At 0.0 Squash, each diffusion step receives the full intensity you set from Skew and Stretch, while 1.0 Squash ensures each step has only the original step size output by the model. This setting will only remove intensity unless you have a non-zero Skew value. Squash will 'soften' the effects of Skew and Stretch as it's raised, but the 'removed' Skew and Stretch intensity is replaced by enhanced micro-detailing and 'burn'. Squash should generally be left low and used as a 'finishing' step after dialing in a decent Skew and Stretch value.
|
||||
|
||||
## Beginner How-To
|
||||
1. Set Skew to your normal CFG Scale setting and Stretch to 1/2 your normal CFG Scale. Set Squash to 0.0.<br>
|
||||
*Alternatively, try starting at 1/1/0.0 to get a baseline.*
|
||||
1. Set Skew to 1/2 of your normal CFG Scale setting and Stretch to your full normal CFG Scale. Set Squash to 0.0.<br>
|
||||
*Alternatively, try starting with the default of 2/5/0.75, or at 1/1/1 to get a baseline.*
|
||||
2. Test some outputs. Results should be similar in quality to CFG.
|
||||
3. Adjust Skew to change the intensity of your outputs adherence to your positive and negative prompts. This primarily effects composition of the output.
|
||||
4. Adjust Stretch to intensify your positive prompt's aspects and colors where they differ from the negative prompt. This primarily effects color and texture.
|
||||
5. Adjust Squash to soften Skew and Stretch's effects. The intensity removed from Skew and Stretch will generally become additional micro-detailing and elements.
|
||||
|
||||
> [!TIP]
|
||||
> [!TIP]
|
||||
> You can experiment with negative values for each setting as well. This can be useful to understand how the model interpreting your negative prompt.
|
||||
|
||||
> [!WARNING]
|
||||
> [!WARNING]
|
||||
> Don't set NRS values to negatives if there are things in your negative prompt you **actually** don't want to see.
|
||||
|
||||
## Setup & Installation
|
||||
|
||||
### ComfyUI
|
||||
<details>
|
||||
<summary>ComfyUI Setup Instructions</summary>
|
||||
|
||||
#### Installation
|
||||
Install via ComfyUI Manager or manually clone this repository into your `ComfyUI/custom_nodes/` directory.
|
||||
|
||||
#### Usage
|
||||
1. **Important**: Ignore the CFG setting on your KSampler node - NRS replaces CFG entirely
|
||||
2. Connect your model through the **Negative Rejection Steering** node before sampling
|
||||
3. Configure NRS parameters (Skew/Stretch/Squash) instead of using CFG
|
||||
|
||||
#### Basic Workflow
|
||||
```
|
||||
Model → NRS Node → KSampler
|
||||
```
|
||||
|
||||
**Pro tip**: To verify NRS is working correctly, set CFG to an extremely high value (like 30). If your output looks normal, NRS is functioning properly. If the output appears "turbo fried," check your node connections.
|
||||
|
||||
**Sampler Compatibility**: NRS now supports advanced samplers including WanKSamplerAdvanced, RES4LYF samplers, and FLOW models (WAN21, Flux) with enhanced prediction type detection.
|
||||
|
||||

|
||||
</details>
|
||||
|
||||
### Automatic1111 / Forge / reForge
|
||||
<details>
|
||||
<summary>WebUI Setup Instructions</summary>
|
||||
|
||||
#### Installation
|
||||
1. Install the extension through the Extensions tab in your WebUI
|
||||
2. Enable the extension and restart your WebUI
|
||||
|
||||
#### Usage
|
||||
Once installed and enabled, the NRS settings panel will appear in your generation interface. When NRS is active:
|
||||
- **CFG Scale is ignored** - the WebUI may still show the CFG setting, but it has no effect
|
||||
- Use the NRS parameters (Skew/Stretch/Squash) to control generation instead
|
||||
- Follow the same parameter guidelines from the [Beginner How-To](#beginner-how-to) section
|
||||
</details>
|
||||
|
||||
### 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.
|
||||
|
||||
## Submitted User Examples
|
||||
| User | CFG | NRS |
|
||||
| --- | --- | --- |
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@ from .NRS.nodes_NRS import *
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"NRS": NRS}
|
||||
NODE_DISPLAY_NAME_MAPPINS = {"NRS": "Negative Rejection Steering"}
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
|
||||
@@ -8,4 +8,4 @@ This is the sister knob to Skew. This is the accelerator. We want to go 'faster'
|
||||
[^1]: This is more true in eps than v-pred. Stretch is inherently more powerful in v-pred based models than eps models.
|
||||
|
||||
## SQUASH
|
||||
This is out 'safety' knob. Think of this like a 'limiter' in a car. This sets the 'top speed' we can go to some multiple of the 'default' speed the model would 'like to' go. i.e. whatever length of directional vector the model produces prior to any skewing or stretching is treated as the 'default' length with Squash=1.0 ensuring we only every go that 'speed' and no more, while Squash=0.0 lets us go any speed we want based on the other 2 knobs. GENERALLY we'll be leaving Squash at 0.0 unless we need it for specific generations.
|
||||
This is out 'safety' knob. Think of this like a 'limiter' in a car. This sets the 'top speed' we can go to some multiple of the 'default' speed the model would 'like to' go. i.e. whatever length of directional vector the model produces prior to any skewing or stretching is treated as the 'default' length with Squash=1.0 ensuring we only every go that 'speed' and no more, while Squash=0.0 lets us go any speed we want based on the other 2 knobs. GENERALLY we'll be leaving Squash at 0.0 unless we need it for specific generations.
|
||||
|
||||
+44
-1
@@ -1,8 +1,12 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
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."
|
||||
authors = [{name = "Bryan O'Malley", email = "bo122081@hotmail.com"}]
|
||||
version = "0.7.0"
|
||||
version = "0.7.4"
|
||||
license = {file = "LICENSE"}
|
||||
readme = "README.md"
|
||||
|
||||
@@ -10,7 +14,46 @@ readme = "README.md"
|
||||
Repository = "https://github.com/Reithan/negative_rejection_steering"
|
||||
# Used by Comfy Registry https://comfyregistry.org
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
include = ["NRS*", "scripts*"]
|
||||
exclude = ["tests*", "Examples*"]
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "reithan"
|
||||
DisplayName = "Negative Rejection Steering"
|
||||
Icon = "https://raw.githubusercontent.com/Reithan/negative_rejection_steering/main/icon.png"
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pre-commit>=3.7.0",
|
||||
"pytest>=8.0.0",
|
||||
"ruff>=0.6.0",
|
||||
]
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
target-version = "py310"
|
||||
|
||||
[tool.ruff.lint]
|
||||
select = [
|
||||
"E", # pycodestyle errors
|
||||
"W", # pycodestyle warnings
|
||||
"F", # pyflakes
|
||||
"I", # isort
|
||||
"N", # pep8-naming
|
||||
"UP", # pyupgrade
|
||||
]
|
||||
ignore = [
|
||||
"E501", # line too long (handled by formatter)
|
||||
]
|
||||
|
||||
[tool.ruff.lint.per-file-ignores]
|
||||
"__init__.py" = ["F401", "F403", "F405"] # Allow star imports and undefined names in __init__.py (ComfyUI pattern)
|
||||
"NRS/nodes_NRS.py" = ["N802", "N804"] # Allow INPUT_TYPES and 's' parameter naming (ComfyUI API convention)
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
testpaths = ["tests"]
|
||||
python_files = "test_*.py"
|
||||
python_functions = "test_*"
|
||||
addopts = "-v --tb=short"
|
||||
norecursedirs = [".git", ".venv", "NRS", "scripts", "Examples"]
|
||||
|
||||
@@ -1,19 +1,22 @@
|
||||
import gradio as gr
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
from functools import partial
|
||||
from modules import scripts, script_callbacks
|
||||
from typing import Any
|
||||
|
||||
import gradio as gr
|
||||
from modules import script_callbacks, scripts
|
||||
|
||||
from NRS.nodes_NRS import NRS
|
||||
|
||||
|
||||
class NRSScript(scripts.Script):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.enabled = False
|
||||
self.skew = 4.0
|
||||
self.stretch = 2.0
|
||||
self.squash = 0.0
|
||||
self.skew = 2.00
|
||||
self.stretch = 5.00
|
||||
self.squash = 0.75
|
||||
|
||||
sorting_priority = 5
|
||||
|
||||
@@ -22,22 +25,39 @@ class NRSScript(scripts.Script):
|
||||
|
||||
def show(self, is_img2img):
|
||||
return scripts.AlwaysVisible
|
||||
|
||||
|
||||
def ui(self, *args, **kwargs):
|
||||
with gr.Accordion(open=False, label=self.title()):
|
||||
enabled = gr.Checkbox(label="Enable NRS", value=self.enabled)
|
||||
gr.HTML("<p><i>Adjust the settings for Negative Rejection Steering.</i></p>")
|
||||
skew = gr.Slider(label="NRS Skew Scale", info="Adjusts the amount guidance is steered.", minimum=-30.0, maximum=30.0, step=0.01, value=self.skew)
|
||||
stretch = gr.Slider(label="NRS Stretch Scale", info="Adjusts the amount guidance is amplified.", minimum=-30.0, maximum=30.0, step=0.01, value=self.stretch)
|
||||
squash = gr.Slider(label="NRS Squash Multiplier", info="Adjusts the amount final guidance is normalized.", minimum=0.0, maximum=1.0, step=0.01, value=self.squash)
|
||||
skew = gr.Slider(
|
||||
label="NRS Skew Scale",
|
||||
info="Adjusts the amount guidance is steered.",
|
||||
minimum=-30.0,
|
||||
maximum=30.0,
|
||||
step=0.01,
|
||||
value=self.skew,
|
||||
)
|
||||
stretch = gr.Slider(
|
||||
label="NRS Stretch Scale",
|
||||
info="Adjusts the amount guidance is amplified.",
|
||||
minimum=-30.0,
|
||||
maximum=30.0,
|
||||
step=0.01,
|
||||
value=self.stretch,
|
||||
)
|
||||
squash = gr.Slider(
|
||||
label="NRS Squash Multiplier",
|
||||
info="Adjusts the amount final guidance is normalized.",
|
||||
minimum=0.0,
|
||||
maximum=1.0,
|
||||
step=0.01,
|
||||
value=self.squash,
|
||||
)
|
||||
|
||||
enabled.change(
|
||||
lambda x: self.update_enabled(x),
|
||||
inputs=[enabled]
|
||||
)
|
||||
enabled.change(lambda x: self.update_enabled(x), inputs=[enabled])
|
||||
|
||||
return (enabled, skew, stretch, squash)
|
||||
|
||||
|
||||
def update_enabled(self, value):
|
||||
self.enabled = value
|
||||
@@ -70,22 +90,28 @@ class NRSScript(scripts.Script):
|
||||
unet = NRS().patch(unet, self.skew, self.stretch, self.squash)[0]
|
||||
|
||||
p.sd_model.forge_objects.unet = unet
|
||||
p.extra_generation_params.update({
|
||||
"NRS_enabled": True,
|
||||
"NRS_skew": self.skew,
|
||||
"NRS_stretch": self.stretch,
|
||||
"NRS_squash": self.squash,
|
||||
})
|
||||
p.extra_generation_params.update(
|
||||
{
|
||||
"NRS_enabled": True,
|
||||
"NRS_skew": self.skew,
|
||||
"NRS_stretch": self.stretch,
|
||||
"NRS_squash": self.squash,
|
||||
}
|
||||
)
|
||||
|
||||
logging.debug(f"NRS: Enabled: {self.enabled}, Squash: {self.skew}, Stretch: {self.stretch}, Squash: {self.squash}")
|
||||
logging.debug(
|
||||
f"NRS: Enabled: {self.enabled}, Squash: {self.skew}, Stretch: {self.stretch}, Squash: {self.squash}"
|
||||
)
|
||||
|
||||
return
|
||||
|
||||
|
||||
def set_value(p, x: Any, xs: Any, *, field: str):
|
||||
if not hasattr(p, "_nrs_xyz"):
|
||||
p._nrs_xyz = {}
|
||||
p._nrs_xyz[field] = x
|
||||
|
||||
|
||||
def make_axis_on_xyz_grid():
|
||||
xyz_grid = None
|
||||
for script in scripts.scripts_data:
|
||||
@@ -98,10 +124,7 @@ def make_axis_on_xyz_grid():
|
||||
|
||||
axis = [
|
||||
xyz_grid.AxisOption(
|
||||
"(NRS) Enabled",
|
||||
str,
|
||||
partial(set_value, field="enabled"),
|
||||
choices=lambda: ["True", "False"]
|
||||
"(NRS) Enabled", str, partial(set_value, field="enabled"), choices=lambda: ["True", "False"]
|
||||
),
|
||||
xyz_grid.AxisOption(
|
||||
"(NRS) Skew",
|
||||
@@ -123,6 +146,7 @@ def make_axis_on_xyz_grid():
|
||||
if not any(x.label.startswith("(NRS)") for x in xyz_grid.axis_options):
|
||||
xyz_grid.axis_options.extend(axis)
|
||||
|
||||
|
||||
def on_before_ui():
|
||||
try:
|
||||
make_axis_on_xyz_grid()
|
||||
@@ -133,4 +157,5 @@ def on_before_ui():
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
|
||||
script_callbacks.on_before_ui(on_before_ui)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Pytest configuration - mock torch and gradio since they're provided by ComfyUI/WebUI at runtime."""
|
||||
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# Create mock base class for WebUI Script
|
||||
class MockScript:
|
||||
"""Mock base class for WebUI scripts."""
|
||||
|
||||
AlwaysVisible = "AlwaysVisible" # Mock the AlwaysVisible constant
|
||||
|
||||
|
||||
# Mock torch, gradio, and WebUI modules before any imports happen
|
||||
# This allows pytest to discover and import tests without these heavy dependencies
|
||||
sys.modules["torch"] = MagicMock()
|
||||
|
||||
# Mock gradio with return_value configured for common patterns
|
||||
mock_gradio = MagicMock()
|
||||
mock_gradio.Accordion = MagicMock(return_value=MagicMock(__enter__=MagicMock(), __exit__=MagicMock()))
|
||||
sys.modules["gradio"] = mock_gradio
|
||||
|
||||
# Mock WebUI modules with proper base class
|
||||
mock_modules = MagicMock()
|
||||
mock_modules.scripts = MagicMock()
|
||||
mock_modules.scripts.Script = MockScript
|
||||
mock_modules.script_callbacks = MagicMock()
|
||||
sys.modules["modules"] = mock_modules
|
||||
sys.modules["modules.scripts"] = mock_modules.scripts
|
||||
sys.modules["modules.script_callbacks"] = mock_modules.script_callbacks
|
||||
@@ -0,0 +1,111 @@
|
||||
"""Regression tests for NRS._get_pred_type and _RAW_TO_ENUM mappings.
|
||||
|
||||
These tests pin CURRENT behavior (flow-family names resolve to EPS) as a
|
||||
safety net ahead of the FLOW reclassification planned for a later PR. If
|
||||
this file needs updating because flow-family names now map to
|
||||
PredictionType.FLOW, that is expected -- it means the reclassification
|
||||
landed and this net did its job.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
# Add project root to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from NRS.nodes_NRS import _RAW_TO_ENUM, NRS, PredictionType
|
||||
|
||||
|
||||
def _make_model_sampling(class_name):
|
||||
"""Build an instance whose type name matches class_name, for class-name fingerprinting."""
|
||||
return type(class_name, (object,), {})()
|
||||
|
||||
|
||||
class _StubModel:
|
||||
"""Minimal stand-in for a model object walked by _get_pred_type."""
|
||||
|
||||
def __init__(self, model_type=None, model_sampling=None, inner_model_type=None):
|
||||
if model_type is not None:
|
||||
self.model_type = model_type
|
||||
if model_sampling is not None:
|
||||
self.model_sampling = model_sampling
|
||||
if inner_model_type is not None:
|
||||
# Emulates model.model.model_type used by the enhanced-detection fallback.
|
||||
self.model = _StubModel(model_type=inner_model_type)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"raw,expected",
|
||||
[
|
||||
("eps", PredictionType.EPS),
|
||||
("epsilon", PredictionType.EPS),
|
||||
("flux", PredictionType.EPS),
|
||||
("chroma", PredictionType.EPS),
|
||||
("flow", PredictionType.EPS),
|
||||
("wan", PredictionType.EPS),
|
||||
("const", PredictionType.EPS),
|
||||
("v", PredictionType.V),
|
||||
("v_prediction", PredictionType.V),
|
||||
("x0", PredictionType.X0),
|
||||
("sample", PredictionType.X0),
|
||||
],
|
||||
)
|
||||
def test_raw_to_enum_mapping(raw, expected):
|
||||
"""Pin the current _RAW_TO_ENUM dict mappings."""
|
||||
assert _RAW_TO_ENUM[raw] == expected
|
||||
|
||||
|
||||
def test_raw_to_enum_unknown_raw_not_present():
|
||||
"""Unrecognized raw strings are not in the dict; callers fall back to UNKNOWN."""
|
||||
assert "totally-unrecognized" not in _RAW_TO_ENUM
|
||||
|
||||
|
||||
class TestGetPredTypeDirectAttribute:
|
||||
"""_get_pred_type's direct-hit path via model_type/prediction_type/parameterization."""
|
||||
|
||||
def test_model_type_v_prediction(self):
|
||||
node = NRS()
|
||||
model = _StubModel(model_type="v_prediction")
|
||||
assert node._get_pred_type(model) == PredictionType.V
|
||||
|
||||
def test_model_type_eps(self):
|
||||
node = NRS()
|
||||
model = _StubModel(model_type="eps")
|
||||
assert node._get_pred_type(model) == PredictionType.EPS
|
||||
|
||||
def test_model_type_x0(self):
|
||||
node = NRS()
|
||||
model = _StubModel(model_type="x0")
|
||||
assert node._get_pred_type(model) == PredictionType.X0
|
||||
|
||||
def test_model_type_flow_family_is_currently_eps(self):
|
||||
"""Flow-family models currently resolve to EPS (pre-reclassification)."""
|
||||
model = _StubModel(model_type="flow")
|
||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||
|
||||
def test_model_type_wan_is_currently_eps(self):
|
||||
model = _StubModel(model_type="wan")
|
||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||
|
||||
|
||||
class TestGetPredTypeEnhancedDetectionFallback:
|
||||
"""The model_sampling class-name and model.model.model_type fallback paths."""
|
||||
|
||||
def test_model_sampling_const_class_is_eps(self):
|
||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingContinuousEDMConst"))
|
||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||
|
||||
def test_model_sampling_v_prediction_class_is_v(self):
|
||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingV_Prediction"))
|
||||
assert NRS()._get_pred_type(model) == PredictionType.V
|
||||
|
||||
def test_model_sampling_eps_class_is_eps(self):
|
||||
model = _StubModel(model_sampling=_make_model_sampling("ModelSamplingEps"))
|
||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||
|
||||
def test_unrecognized_model_defaults_to_eps(self):
|
||||
"""Fully-unrecognized models fall back to EPS (documented default)."""
|
||||
model = _StubModel()
|
||||
assert NRS()._get_pred_type(model) == PredictionType.EPS
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Functional tests for NRS ComfyUI node and WebUI script."""
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# Add project root to path for imports
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
|
||||
def test_nrs_node_comfyui_interface():
|
||||
"""Test that NRS node has required ComfyUI interface."""
|
||||
from NRS.nodes_NRS import NRS
|
||||
|
||||
# Test class can be instantiated
|
||||
node = NRS()
|
||||
assert node is not None
|
||||
|
||||
# Test INPUT_TYPES classmethod exists and returns proper structure
|
||||
assert hasattr(NRS, "INPUT_TYPES")
|
||||
assert callable(NRS.INPUT_TYPES)
|
||||
input_types = NRS.INPUT_TYPES()
|
||||
assert isinstance(input_types, dict)
|
||||
assert "required" in input_types
|
||||
assert "model" in input_types["required"]
|
||||
assert "skew" in input_types["required"]
|
||||
assert "stretch" in input_types["required"]
|
||||
assert "squash" in input_types["required"]
|
||||
|
||||
# Test patch method exists with correct signature
|
||||
assert hasattr(node, "patch")
|
||||
assert callable(node.patch)
|
||||
sig = inspect.signature(node.patch)
|
||||
params = list(sig.parameters.keys())
|
||||
assert "model" in params
|
||||
assert "skew" in params
|
||||
assert "stretch" in params
|
||||
assert "squash" in params
|
||||
|
||||
# Test required class attributes
|
||||
assert hasattr(NRS, "RETURN_TYPES")
|
||||
assert NRS.RETURN_TYPES == ("MODEL",)
|
||||
assert hasattr(NRS, "FUNCTION")
|
||||
assert NRS.FUNCTION == "patch"
|
||||
assert hasattr(NRS, "CATEGORY")
|
||||
assert NRS.CATEGORY == "advanced/model"
|
||||
|
||||
|
||||
def test_nrs_script_webui_interface():
|
||||
"""Test that NRSScript has required WebUI/Gradio interface."""
|
||||
from scripts.negative_rejection_steering_script import NRSScript
|
||||
|
||||
# Test class can be instantiated
|
||||
script = NRSScript()
|
||||
assert script is not None
|
||||
|
||||
# Test required methods exist
|
||||
assert hasattr(script, "title")
|
||||
assert callable(script.title)
|
||||
assert isinstance(script.title(), str)
|
||||
|
||||
assert hasattr(script, "show")
|
||||
assert callable(script.show)
|
||||
|
||||
assert hasattr(script, "ui")
|
||||
assert callable(script.ui)
|
||||
|
||||
assert hasattr(script, "process_before_every_sampling")
|
||||
assert callable(script.process_before_every_sampling)
|
||||
|
||||
# Test process_before_every_sampling has correct signature
|
||||
sig = inspect.signature(script.process_before_every_sampling)
|
||||
params = list(sig.parameters.keys())
|
||||
# Note: 'self' is not included in signature, only other parameters
|
||||
assert "p" in params
|
||||
|
||||
|
||||
def test_prediction_type_enum():
|
||||
"""Test PredictionType enum has required values."""
|
||||
from NRS.nodes_NRS import PredictionType
|
||||
|
||||
# Test enum has required prediction types
|
||||
assert hasattr(PredictionType, "EPS")
|
||||
assert hasattr(PredictionType, "V")
|
||||
assert hasattr(PredictionType, "X0")
|
||||
assert hasattr(PredictionType, "UNKNOWN")
|
||||
|
||||
# Test enum values are distinct
|
||||
assert PredictionType.EPS != PredictionType.V
|
||||
assert PredictionType.V != PredictionType.X0
|
||||
assert PredictionType.X0 != PredictionType.UNKNOWN
|
||||
@@ -0,0 +1,262 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.11"
|
||||
|
||||
[[package]]
|
||||
name = "cfgv"
|
||||
version = "3.5.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorama"
|
||||
version = "0.4.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "distlib"
|
||||
version = "0.4.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" }
|
||||
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" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "filelock"
|
||||
version = "3.29.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.19"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "negative-rejection-steering"
|
||||
version = "0.7.4"
|
||||
source = { editable = "." }
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pre-commit" },
|
||||
{ name = "pytest" },
|
||||
{ name = "ruff" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=3.7.0" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
|
||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.0" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "nodeenv"
|
||||
version = "1.10.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "platformdirs"
|
||||
version = "4.9.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
{ name = "identify" },
|
||||
{ name = "nodeenv" },
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
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 = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-discovery"
|
||||
version = "1.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/48/60/e88788207d81e46362cfbef0d4aaf4c0f49efc3c12d4c3fa3f542c34ebec/python_discovery-1.3.1.tar.gz", hash = "sha256:62f6db28064c9613e7ca76cb3f00c38c839a07c31c00dfe7ed0986493d2150a6", size = 68011, upload-time = "2026-05-12T20:53:36.336Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/6f/a05a317a66fee0aad270011461f1a63a453ed12471249f172f7d2e2bc7b4/python_discovery-1.3.1-py3-none-any.whl", hash = "sha256:ed188687ebb3b82c01a17cd5ac62fc94d9f6487a7f1a0f9dfe89753fec91039c", size = 33185, upload-time = "2026-05-12T20:53:34.969Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyyaml"
|
||||
version = "6.0.3"
|
||||
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" }
|
||||
wheels = [
|
||||
{ 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/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/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.15.13"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/21/a7d5c126d5b557715ef81098f3db2fe20f622a039ff2e626af28d674ab80/ruff-0.15.13.tar.gz", hash = "sha256:f9d89f17f7ba7fb2ed42921f0df75da797a9a5d71bc39049e2c687cf2baf44b7", size = 4678180, upload-time = "2026-05-14T13:44:37.869Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/61/11d458dc6ac22504fd8e237b29dfd40504c7fbbcc8930402cfe51a8e63ed/ruff-0.15.13-py3-none-linux_armv6l.whl", hash = "sha256:444b580fc72fd6887e650acd3e575e18cdc79dbcf42fb4030b491057921f61f8", size = 10738279, upload-time = "2026-05-14T13:44:18.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/ca/caa871ee7be718c45256fada4e16a218ee3e33f0c4a46b729a60a24912e6/ruff-0.15.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6590d009e7cb7ebf36f83dbdd44a3fa48a0994ff6f1cdc1b08006abe58f98dc7", size = 11124798, upload-time = "2026-05-14T13:44:06.427Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/19/43f5f2e568dddde567fc41f8471f9432c09563e19d3e617a48cfa52f8f0a/ruff-0.15.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1c26d2f66163deeb6e08d8b39fbbe983ce3c71cea06a6d7591cfd1421793c629", size = 10460761, upload-time = "2026-05-14T13:44:04.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/df/cf938cd6de3003178f03ad7c1ea2a6c099468c03a35037985070b37e76be/ruff-0.15.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9dbd6f94b434f896308e4d57fb7bfde0d02b99f7a64b3bdab0fdfa6a864203a5", size = 10804451, upload-time = "2026-05-14T13:44:25.221Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/7d/5d0973129b154ded2225729169d7068f26b467760b146493fde138415f23/ruff-0.15.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3259f3be4d181bda591da5db2571aed6853c6a048157756448020bc6c5cd22", size = 10534285, upload-time = "2026-05-14T13:44:08.888Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/e3/6b999bbc66cd51e5f073842bc2a3995e99c5e0e72e16b15e7261f7abf57a/ruff-0.15.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae9c17e5eb4430c154e76abc25d79a318190f5a997f38fb6b114416c5319ffc9", size = 11312063, upload-time = "2026-05-14T13:44:11.274Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/5a/642639e9f5db04f1e97fbd6e091c6fd20725bdf072fb114d00eefb9e6eb8/ruff-0.15.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2e2e39bff6c341f4b577a21b801326fab0b11847f48fcaa83f00a113c9b3cb55", size = 12183079, upload-time = "2026-05-14T13:44:01.634Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/4c/7585735f6b53b0f12de13618b2f7d250a844f018822efc899df2e7b8295f/ruff-0.15.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e8d9a8e08013542e94d3220bc5b62cc3e5ef87c5f74bff367d3fac14fab013e6", size = 11440833, upload-time = "2026-05-14T13:43:59.043Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/31/bf1a0803d077e679cfeee5f2f67290a0fa79c7385b5d9a8c17b9db2c48f0/ruff-0.15.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc411dfebe5eebe55ce041c6ae080eb7668955e866daa2fbb16692a784f1c4ca", size = 11434486, upload-time = "2026-05-14T13:44:27.761Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/4e/62c9b999875d4f14db80f277c030578f5e249c9852d65b7ac7ad0b43c041/ruff-0.15.13-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:768494eb08b9cee54e2fd27969966f74db5a57f6eaa7a90fcb3306af34dfc4bd", size = 11385189, upload-time = "2026-05-14T13:44:13.704Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/89/7e959047a104df3eb12863447c110140191fc5b6c4f379ea2e803fcdb0e4/ruff-0.15.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:fb75f9a3a7e42ffe117d734494e6c5e5cb3565d66e12612cb63d0e572a41a5b6", size = 10781380, upload-time = "2026-05-14T13:43:56.734Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/52/5fd18f3b88cab63e88aa11516b3b4e1e5f720e5c330f8dbe5c26210f41f8/ruff-0.15.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8cb74dd33bb2f6613faf7fc03b660053b5ac4f80e706d5788c6335e2a8048d51", size = 10540605, upload-time = "2026-05-14T13:44:20.748Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/e0/9e35f338990d3e41a82875ff7053ffe97541dae81c9d02143177f381d572/ruff-0.15.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7ef823f817fcd191dc934e984be9cf4094f808effa16f2542ad8e821ba02bbf2", size = 11036554, upload-time = "2026-05-14T13:44:16.256Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/13/070fb048c24080fba188f66371e2a92785be257ad02242066dc7255ac6e9/ruff-0.15.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f345a13937bd7f09f6f5d19fa0721b0c103e00e7f62bc67089a8e5e037719e0b", size = 11528133, upload-time = "2026-05-14T13:44:22.808Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/8c/b1e1666aef7fc6555094d73ae6cd981701781ae85b97ceefc0eebd0b4668/ruff-0.15.13-py3-none-win32.whl", hash = "sha256:4044f94208b3b05ba0fc4a4abd0558cf4d6459bd18325eead7fd8cc66f909b41", size = 10721455, upload-time = "2026-05-14T13:44:35.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a6/870a3e8a50590bb92be184ad928c2922f088b00d9dc5c5ec7b924ee08c22/ruff-0.15.13-py3-none-win_amd64.whl", hash = "sha256:7064884d442b7d477b4e7473d12da7f08851d2b1982763c5d3f388a19468a1a4", size = 11900409, upload-time = "2026-05-14T13:44:30.389Z" },
|
||||
{ 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 = "virtualenv"
|
||||
version = "21.3.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "distlib" },
|
||||
{ name = "filelock" },
|
||||
{ name = "platformdirs" },
|
||||
{ name = "python-discovery" },
|
||||
]
|
||||
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 = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/34/a9dbe051de88a63eb7408ea66630bac38e72f7f6077d4be58737106860d9/virtualenv-21.3.3-py3-none-any.whl", hash = "sha256:7d5987d8369e098e41406efb780a3d4ca79280097293899e351a6407ee153ab3", size = 7594554, upload-time = "2026-05-13T18:01:27.815Z" },
|
||||
]
|
||||
Reference in New Issue
Block a user