[no ci] Enhance dependency management in upgrade_deps workflow

Added support for parsing TOML files using tomllib and updated dependency handling logic to include optional and development dependencies.
This commit is contained in:
Jay Lee
2026-05-15 19:49:32 -04:00
committed by GitHub
parent 7e8042a2f8
commit ee3bb42d19

View File

@@ -28,6 +28,7 @@ jobs:
import json import json
import urllib.request import urllib.request
import re import re
import tomllib
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from pathlib import Path from pathlib import Path
@@ -37,27 +38,37 @@ jobs:
exit(1) exit(1)
content = toml_path.read_text(encoding="utf-8") content = toml_path.read_text(encoding="utf-8")
parsed_toml = tomllib.loads(content)
# Regex to find standard dependencies under [project] dependencies = [ ... ] target_deps = set()
# This handles lines like "urllib3>=1.26", "cryptography", etc. inside the array
dep_block_match = re.search(r'dependencies\s*=\s*\[(.*?)\]', content, re.DOTALL) # Standard [project.dependencies]
if not dep_block_match: target_deps.update(parsed_toml.get("project", {}).get("dependencies", []))
print("No standard project dependencies array found.")
# Optional [project.optional-dependencies]
for deps in parsed_toml.get("project", {}).get("optional-dependencies", {}).values():
target_deps.update(deps)
# Dev [dependency-groups] (uv / PEP 735 standard)
for deps in parsed_toml.get("dependency-groups", {}).values():
if isinstance(deps, list):
for d in deps:
if isinstance(d, str):
target_deps.add(d)
if not target_deps:
print("No dependencies found to process.")
exit(0) exit(0)
dep_block = dep_block_match.group(1) updates = {}
raw_deps = re.findall(r'"([^"]+)"', dep_block)
updated_deps = []
two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14) two_weeks_ago = datetime.now(timezone.utc) - timedelta(days=14)
print(f"Evaluating dependencies against cutoff date: {two_weeks_ago.isoformat()}") print(f"Evaluating dependencies against cutoff date: {two_weeks_ago.isoformat()}")
for dep in raw_deps: for dep in target_deps:
# Isolate base package name (e.g., "urllib3>=1.25" -> "urllib3") # Isolate base package name (e.g., "yubikey-manager>=5.6.1" -> "yubikey-manager")
pkg_name = re.split(r'[<>=!~;]', dep)[0].strip() pkg_name = re.split(r'[<>=!~;\s]', dep)[0].strip()
# Pull environment markers to re-attach later if they exist (e.g., ; sys_platform == 'win32') # Preserve environment markers if they exist
marker = "" marker = ""
if ";" in dep: if ";" in dep:
marker = " ; " + dep.split(";", 1)[1].strip() marker = " ; " + dep.split(";", 1)[1].strip()
@@ -74,18 +85,16 @@ jobs:
if not files: if not files:
continue continue
# Get upload time of the first file in the release
upload_time_str = files[0].get("upload_time_iso_8601") upload_time_str = files[0].get("upload_time_iso_8601")
if not upload_time_str: if not upload_time_str:
continue continue
# Normalize trailing Z to +00:00 for Python's datetime parser
if upload_time_str.endswith('Z'): if upload_time_str.endswith('Z'):
upload_time_str = upload_time_str[:-1] + '+00:00' upload_time_str = upload_time_str[:-1] + '+00:00'
upload_time = datetime.fromisoformat(upload_time_str) upload_time = datetime.fromisoformat(upload_time_str)
# Filter: Must be older than 2 weeks and not a pre-release (alpha/beta/rc) # Filter: Must be older than 2 weeks and not a pre-release
if upload_time <= two_weeks_ago and not any(x in ver.lower() for x in ['a', 'b', 'rc', 'dev']): if upload_time <= two_weeks_ago and not any(x in ver.lower() for x in ['a', 'b', 'rc', 'dev']):
valid_versions.append((upload_time, ver)) valid_versions.append((upload_time, ver))
@@ -95,20 +104,34 @@ jobs:
target_version = valid_versions[0][1] target_version = valid_versions[0][1]
pinned_dep = f"{pkg_name}=={target_version}{marker}" pinned_dep = f"{pkg_name}=={target_version}{marker}"
updated_deps.append(f' "{pinned_dep}",') if pinned_dep != dep:
print(f" -> Pinned to {target_version}") updates[dep] = pinned_dep
print(f" -> Pinning: '{dep}' => '{pinned_dep}'")
else: else:
# Fallback to original line if no historical versions found print(f" -> Already pinned correctly to {target_version}")
updated_deps.append(f' "{dep}",') else:
print(f" -> No valid historical versions found.")
except urllib.error.HTTPError as e:
print(f" -> Package not found on PyPI or HTTP error: {e}")
except Exception as e: except Exception as e:
print(f" -> Error fetching package {pkg_name}: {e}. Keeping original constraint.") print(f" -> Error processing {pkg_name}: {e}")
updated_deps.append(f' "{dep}",')
# 2. Reconstruct the dependencies block in pyproject.toml # 3. Replace the strings safely in the original file content
new_dep_block = "\n" + "\n".join(updated_deps) + "\n" new_content = content
new_content = content.replace(dep_block_match.group(1), new_dep_block) for old_dep, new_dep in updates.items():
# Regex targets the exact string inside either single or double quotes
# Using a lambda replacement ensures we don't trip over escape sequences in the new string
escaped_old = re.escape(old_dep)
pattern = r'([\'"])' + escaped_old + r'\1'
new_content = re.sub(pattern, lambda m: m.group(1) + new_dep + m.group(1), new_content)
# Write changes back to pyproject.toml
if content != new_content:
toml_path.write_text(new_content, encoding="utf-8") toml_path.write_text(new_content, encoding="utf-8")
print("\npyproject.toml updated successfully.")
else:
print("\nNo updates required.")
- name: Create Pull Request - name: Create Pull Request
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1