diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..561cc7642c5916ce630e2637dc1fda3d0cac838d
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,9 @@
+# Ignore datasets and heavy files to keep deployment fast
+dataset/
+__pycache__/
+*.pyc
+.env
+.git
+.vscode
+.gemini
+scripts/testing/mission_dataset_split.zip
diff --git a/.env b/.env
new file mode 100644
index 0000000000000000000000000000000000000000..e7cd2c3e1001a00547ac5ca046d3a6a467fbe0f2
--- /dev/null
+++ b/.env
@@ -0,0 +1,3 @@
+OLLAMA_URL="http://localhost:11434"
+OLLAMA_API_KEY="87b344ea09c540848abd777349d64466.PeZFbKB2Y03ddFyD4YXW0TpT"
+OLLAMA_MODEL="llava-llama3"
diff --git a/.gitattributes b/.gitattributes
index a6344aac8c09253b3b630fb776ae94478aa0275b..d77a8fc06eb84a6ba2702dda53ecbe9cac3f4a1c 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -33,3 +33,8 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
*.zip filter=lfs diff=lfs merge=lfs -text
*.zst filter=lfs diff=lfs merge=lfs -text
*tfevents* filter=lfs diff=lfs merge=lfs -text
+venv/images/imagehash.png filter=lfs diff=lfs merge=lfs -text
+venv/Lib/site-packages/__pycache__/typing_extensions.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
+venv/Lib/site-packages/absl/testing/__pycache__/absltest.cpython-311.pyc filter=lfs diff=lfs merge=lfs -text
+venv/Lib/site-packages/aiohttp/_http_parser.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
+venv/Lib/site-packages/aiohttp/_websocket/reader_c.cp311-win_amd64.pyd filter=lfs diff=lfs merge=lfs -text
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000000000000000000000000000000000000..1237ca394363e2fa1694849d9a31e8cd3570ffa9
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,26 @@
+# Use official Python runtime as a parent image
+FROM python:3.11-slim
+
+# Set working directory in the container
+WORKDIR /app
+
+# Install system dependencies (needed for OpenCV/Pillow if required)
+RUN apt-get update && apt-get install -y \
+ libgl1-mesa-glx \
+ libglib2.0-0 \
+ && rm -rf /var/lib/apt/lists/*
+
+# Copy requirements file first (for caching)
+COPY requirements.txt .
+
+# Install dependencies
+RUN pip install --no-cache-dir -r requirements.txt
+
+# Copy the rest of the application code
+COPY . .
+
+# Expose port 7860 for Hugging Face Spaces
+EXPOSE 7860
+
+# Run the application using gunicorn for production
+CMD ["gunicorn", "--bind", "0.0.0.0:7860", "app:app"]
diff --git a/anticheat_hashes.json b/anticheat_hashes.json
new file mode 100644
index 0000000000000000000000000000000000000000..930cf306e954b6fb25b86aa20bfb0218acb2fa78
--- /dev/null
+++ b/anticheat_hashes.json
@@ -0,0 +1 @@
+{"hashes": ["46256446866b53d9", "e0e42531b264e6d6", "e24a3c7a839cef24", "ccdac541d91b9237", "dcdd9c43634f189a", "b3ccd03f4cb11758", "f48b6995d4cf02b1", "525bf524bcb65692", "198964c9cb682cae", "eff64b09e12f8680"]}
\ No newline at end of file
diff --git a/app.py b/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..dadf1cc1238b36b72b29b532adcd412ef665fd2a
--- /dev/null
+++ b/app.py
@@ -0,0 +1,116 @@
+import os
+import traceback
+import logging
+from flask import Flask, request, jsonify
+from flask_cors import CORS
+from werkzeug.utils import secure_filename
+from dotenv import load_dotenv
+
+from utils.anticheat import AntiCheatEngine
+from utils.predictor import Predictor
+from utils.verdict import get_verdict
+
+load_dotenv()
+
+# Setup logging
+logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
+logger = logging.getLogger(__name__)
+
+# Initialize components
+app = Flask(__name__)
+CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True)
+
+ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'webp'}
+app.config['MAX_CONTENT_LENGTH'] = 100 * 1024 * 1024 # Limit upload to 100MB
+
+logger.info("π§ Loading the MISSION 17 AI Brain (Ollama Vision)...")
+anticheat = AntiCheatEngine()
+predictor = Predictor()
+
+def allowed_file(filename):
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
+
+@app.after_request
+def after_request(response):
+ response.headers.add('Access-Control-Allow-Origin', '*')
+ response.headers.add('Access-Control-Allow-Headers', 'Content-Type,Authorization')
+ response.headers.add('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,OPTIONS')
+ return response
+
+@app.route('/health', methods=['GET'])
+def health():
+ return jsonify({
+ "status": "ok",
+ "model": predictor.get_model_name(),
+ "anticheat_hashes": anticheat.count()
+ }), 200
+
+@app.route('/reset-anti-cheat', methods=['POST', 'GET'])
+def reset_anti_cheat():
+ count = anticheat.clear()
+ logger.info("π‘οΈ Anti-cheat hash database cleared!")
+ return jsonify({"message": "Anti-cheat hash database cleared!", "count": count}), 200
+
+@app.route('/predict', methods=['POST'])
+def predict():
+ try:
+ # π CHECK 1: File Presence
+ if 'file' not in request.files:
+ return jsonify({'error': 'No file uploaded'}), 400
+
+ file = request.files['file']
+
+ # π CHECK 2: Empty File Detection (Bug Fix)
+ file.seek(0, os.SEEK_END)
+ if file.tell() == 0:
+ return jsonify({"error": "Processing failed: Empty file"}), 400
+ file.seek(0)
+
+ # π CHECK 3: Empty Filename
+ if file.filename == '':
+ return jsonify({'error': 'No selected file'}), 400
+
+ # π CHECK 4: File Type Validation
+ if not allowed_file(file.filename):
+ return jsonify({'error': 'Invalid file type. Only JPG/PNG allowed.'}), 400
+
+ # Read file bytes ONCE and reuse them
+ file_bytes = file.read()
+
+ # π― MODULE 11: Calculate Hash and Check for Cheaters
+ if anticheat.is_duplicate(file_bytes):
+ logger.warning("π¨ ANTI-CHEAT: Duplicate image detected!")
+ return jsonify({
+ "status": "REJECTED",
+ "error": "Duplicate image detected. You cannot farm points!",
+ "prediction": "Anti-Cheat: Duplicate"
+ }), 400
+
+ # π€ AI Vision Prediction via Ollama
+ logger.info("π€ Sending image to Ollama Vision...")
+ ai_result = predictor.predict(file_bytes)
+
+ category = ai_result.get('category', 'Non_SDG_Invalid')
+ confidence = ai_result.get('confidence', 0)
+ reason = ai_result.get('reason', '')
+
+ # βοΈ Get formatted verdict based on AI output
+ verdict_response = get_verdict(category, confidence, threshold=55)
+ verdict_response['reason'] = reason
+ verdict_response['model'] = predictor.get_model_name()
+
+ # Only register hash if the image was VERIFIED (save memory/prevent false positives on bad images)
+ if verdict_response['is_verified']:
+ anticheat.register(file_bytes)
+ logger.info(f"β Unique verified image logged to anticheat.")
+
+ return jsonify(verdict_response)
+
+ except Exception as e:
+ logger.error(f"β Processing Error: {str(e)}")
+ traceback.print_exc()
+ return jsonify({'error': "Processing failed", 'detail': str(e)}), 500
+
+if __name__ == '__main__':
+ # Hugging Face requires the app to listen on 0.0.0.0:7860
+ app.run(host='0.0.0.0', port=7860, debug=False)
\ No newline at end of file
diff --git a/labels.txt b/labels.txt
new file mode 100644
index 0000000000000000000000000000000000000000..b6de29a85e3dbc653602ee4672f2c91b6596f598
--- /dev/null
+++ b/labels.txt
@@ -0,0 +1,10 @@
+Non_SDG_Invalid
+SDG11_Sustainable_Cities
+SDG12_Recycling
+SDG13_15_Planting
+SDG1_2_Donation
+SDG3_Health_Wellbeing
+SDG4_Quality_Education
+SDG6_14_Cleanup
+SDG7_Clean_Energy
+SDG8_Support_Local
diff --git a/mission_model.h5 b/mission_model.h5
new file mode 100644
index 0000000000000000000000000000000000000000..4bc6f67bae979a625326515801dbf8d41c9acee3
--- /dev/null
+++ b/mission_model.h5
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fb0e4373bc7f9a3d1a9e9d7cacbc1686942d39e2c1debdf2c1b67e1767ef28bc
+size 20967176
diff --git a/outputs/confusion_matrix.png b/outputs/confusion_matrix.png
new file mode 100644
index 0000000000000000000000000000000000000000..0701a9e69f0c7c4b95a40b4f5baff415a89279a1
Binary files /dev/null and b/outputs/confusion_matrix.png differ
diff --git a/outputs/debug_bias_mitigation.jpg b/outputs/debug_bias_mitigation.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3117a940738d2c85653e58227f826e4cc3b84766
Binary files /dev/null and b/outputs/debug_bias_mitigation.jpg differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c4b887e63b2f75429c310605db8012db999574ed
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,10 @@
+Flask==2.2.2
+Flask-Cors==3.0.10
+numpy
+Pillow
+imagehash
+gunicorn
+tensorflow
+scikit-learn
+matplotlib
+seaborn
diff --git a/runtime.txt b/runtime.txt
new file mode 100644
index 0000000000000000000000000000000000000000..2419ad5b0a329db360c988461d3ca3d7c73d6eef
--- /dev/null
+++ b/runtime.txt
@@ -0,0 +1 @@
+3.11.9
diff --git a/scripts/data_prep/balance_dataset.py b/scripts/data_prep/balance_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3a562dc20a2039795fcdc4922ef572ebcea28ac
--- /dev/null
+++ b/scripts/data_prep/balance_dataset.py
@@ -0,0 +1,85 @@
+import os
+import shutil
+import random
+
+# π CONFIGURATION
+CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
+DATASET_DIR = os.path.join(CURRENT_DIR, '..', '..', '..', 'dataset', 'mission_dataset')
+
+# Target: balance ALL classes to this count
+# Set to None to auto-detect the median (safe default)
+TARGET_COUNT = 500
+
+
+def count_classes(dataset_dir):
+ counts = {}
+ for class_name in os.listdir(dataset_dir):
+ class_path = os.path.join(dataset_dir, class_name)
+ if os.path.isdir(class_path):
+ images = [
+ f for f in os.listdir(class_path)
+ if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp'))
+ ]
+ counts[class_name] = len(images)
+ return counts
+
+
+def balance_dataset():
+ print(f"βοΈ Balancing ALL classes in: {DATASET_DIR}\n")
+
+ if not os.path.exists(DATASET_DIR):
+ print("β Error: Dataset folder not found.")
+ return
+
+ counts = count_classes(DATASET_DIR)
+
+ if not counts:
+ print("β No class folders found.")
+ return
+
+ # Determine target
+ target = TARGET_COUNT
+ if target is None:
+ sorted_counts = sorted(counts.values())
+ target = sorted_counts[len(sorted_counts) // 2] # median
+ print(f"π Auto-detected target (median): {target} images per class\n")
+ else:
+ print(f"π Target: {target} images per class\n")
+
+ print(f"{'Class':<35} {'Before':>8} {'After':>8} {'Action'}")
+ print("-" * 65)
+
+ for class_name, count in sorted(counts.items()):
+ class_path = os.path.join(DATASET_DIR, class_name)
+ images = [
+ f for f in os.listdir(class_path)
+ if f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp'))
+ ]
+
+ if count > target:
+ # Trim to target β shuffle first to keep a random selection
+ random.shuffle(images)
+ excess = images[target:]
+ for img in excess:
+ os.remove(os.path.join(class_path, img))
+ after = target
+ action = f"βοΈ Trimmed -{len(excess)}"
+
+ elif count < target:
+ # Class is under-represented β warn user to add more images
+ after = count
+ action = f"β οΈ Under-represented (need +{target - count} more images)"
+
+ else:
+ after = count
+ action = "β OK"
+
+ print(f"{class_name:<35} {count:>8} {after:>8} {action}")
+
+ print("\n⨠Balancing complete! Now run train_ai.py to retrain the model.")
+ print(" π‘ TIP: For under-represented classes, collect more real photos")
+ print(" or use Google Images to download additional training data.")
+
+
+if __name__ == "__main__":
+ balance_dataset()
diff --git a/scripts/data_prep/collect_data.py b/scripts/data_prep/collect_data.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ba66a2206ff7db0c5382e1e740fa899b3ca2df2
--- /dev/null
+++ b/scripts/data_prep/collect_data.py
@@ -0,0 +1,221 @@
+from bing_image_downloader import downloader
+import os
+import shutil
+import math
+
+# π CONFIGURATION
+BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', 'dataset', 'mission_dataset')
+
+# Target images per class β everything will be brought up to this number
+TARGET = 500
+
+# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+# CLASS DEFINITIONS
+# Each class has:
+# "current": how many images it already has
+# "terms": list of 5 search terms to download from
+#
+# limit_per_term = ceil((TARGET - current) / len(terms))
+# Classes already at TARGET are automatically skipped.
+# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+CLASSES = {
+
+ # π± SDG 1/2: Donation β WEAKEST CLASS (150 β 500, needs +350, 70/term)
+ "SDG1_2_Donation": {
+ "current": 150,
+ "terms": [
+ "people donating food to community",
+ "clothes donation box charity",
+ "feeding program volunteers serving food",
+ "grocery donation drive event",
+ "charity relief goods distribution"
+ ]
+ },
+
+ # ποΈ SDG 6/14: Cleanup β WEAKEST CLASS (150 β 500, needs +350, 70/term)
+ "SDG6_14_Cleanup": {
+ "current": 150,
+ "terms": [
+ "beach cleanup volunteers collecting trash",
+ "river cleanup community activity",
+ "coastal cleanup garbage bags collected",
+ "people picking up litter shoreline",
+ "estero creek waterway cleanup"
+ ]
+ },
+
+ # π SDG 4: Education (253 β 500, needs +247, 50/term)
+ "SDG4_Quality_Education": {
+ "current": 253,
+ "terms": [
+ "student reading open book",
+ "teacher writing on whiteboard classroom",
+ "group study session library",
+ "hand writing notes in notebook",
+ "child using educational tablet learning"
+ ]
+ },
+
+ # π SDG 3: Health (262 β 500, needs +238, 48/term)
+ "SDG3_Health_Wellbeing": {
+ "current": 262,
+ "terms": [
+ "people jogging in park",
+ "group yoga session outdoors",
+ "eating fresh fruit salad bowl",
+ "drinking glass of water healthy",
+ "washing hands with soap hygiene"
+ ]
+ },
+
+ # ποΈ SDG 8: Support Local (267 β 500, needs +233, 47/term)
+ "SDG8_Support_Local": {
+ "current": 267,
+ "terms": [
+ "buying from street food vendor",
+ "shopping at local farmers market",
+ "artisan crafting handmade goods",
+ "small bakery local shop front",
+ "supporting small business community"
+ ]
+ },
+
+ # π± SDG 13/15: Planting (270 β 500, needs +230, 46/term)
+ "SDG13_15_Planting": {
+ "current": 270,
+ "terms": [
+ "person planting tree sapling",
+ "community tree planting activity",
+ "garden seedling transplanting soil",
+ "plant growing hands holding soil",
+ "reforestation volunteers planting trees"
+ ]
+ },
+
+ # ποΈ SDG 11: Sustainable Cities (275 β 500, needs +225, 45/term)
+ "SDG11_Sustainable_Cities": {
+ "current": 275,
+ "terms": [
+ "riding bicycle on city road",
+ "passengers inside public city bus",
+ "waiting at train station platform",
+ "walking on pedestrian crossing street",
+ "segregated bike lane urban city"
+ ]
+ },
+
+ # β‘ SDG 7: Clean Energy (277 β 500, needs +223, 45/term)
+ "SDG7_Clean_Energy": {
+ "current": 277,
+ "terms": [
+ "solar panels on house roof",
+ "hand turning off light switch",
+ "electric vehicle charging station",
+ "wind turbine farm landscape",
+ "modern led light bulb energy saving"
+ ]
+ },
+
+ # π« Non-SDG Invalid (430 β 500, needs +70, 14/term)
+ "Non_SDG_Invalid": {
+ "current": 430,
+ "terms": [
+ "random indoor selfie photo",
+ "luxury sports car fast",
+ "video game screenshot gaming",
+ "cat sleeping on sofa",
+ "abstract digital art wallpaper"
+ ]
+ },
+
+ # β»οΈ SDG 12: Recycling β ALREADY AT TARGET (500), will be skipped
+ "SDG12_Recycling": {
+ "current": 500,
+ "terms": []
+ },
+}
+
+# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+print(f"π Smart Data Collection β Target: {TARGET} images per class")
+print(f" Dataset path: {BASE_DIR}\n")
+
+if not os.path.exists(BASE_DIR):
+ print(f"β ERROR: Could not find '{BASE_DIR}'. Check your folder structure.")
+ exit()
+
+total_added = 0
+
+for category, info in CLASSES.items():
+ current = info["current"]
+ terms = info["terms"]
+ needed = TARGET - current
+
+ # Skip classes already at or above target
+ if needed <= 0:
+ print(f"βοΈ [{category}] already at {current}/{TARGET} β SKIPPED\n")
+ continue
+
+ limit_per_term = math.ceil(needed / len(terms))
+ target_dir = os.path.join(BASE_DIR, category)
+ os.makedirs(target_dir, exist_ok=True)
+
+ print(f"π [{category}]")
+ print(f" {current} β {TARGET} | need +{needed} | {limit_per_term} images/term")
+
+ category_added = 0
+
+ for term in terms:
+ print(f" π '{term}' ({limit_per_term} images)...")
+ try:
+ downloader.download(
+ term,
+ limit=limit_per_term,
+ output_dir="temp_downloads",
+ adult_filter_off=True,
+ force_replace=False,
+ timeout=10,
+ verbose=False
+ )
+
+ source_folder = os.path.join("temp_downloads", term)
+ if os.path.exists(source_folder):
+ files = os.listdir(source_folder)
+ moved = 0
+ for file in files:
+ old_path = os.path.join(source_folder, file)
+ if not os.path.isfile(old_path):
+ continue
+ clean_term = term.replace(" ", "_")
+ new_filename = f"{clean_term}_{file}"
+ new_path = os.path.join(target_dir, new_filename)
+ if os.path.exists(new_path):
+ continue # Skip duplicates
+ try:
+ shutil.move(old_path, new_path)
+ moved += 1
+ except Exception:
+ pass
+ print(f" β +{moved} images")
+ category_added += moved
+
+ except Exception as e:
+ print(f" β οΈ Skipped '{term}': {e}")
+
+ # Clean up temp after each term
+ if os.path.exists("temp_downloads"):
+ try:
+ shutil.rmtree("temp_downloads")
+ except Exception:
+ pass
+
+ new_total = current + category_added
+ print(f" π Result: {current} β {new_total} images (+{category_added})\n")
+ total_added += category_added
+
+print("=" * 55)
+print(f"β¨ Done! Total new images added: {total_added}")
+print(f" All classes should now be near {TARGET} images each.")
+print("\n Next steps:")
+print(" 1. python train_ai.py β retrain the model")
+print(" 2. python evaluate_model.py β check accuracy")
\ No newline at end of file
diff --git a/scripts/data_prep/count_dataset.py b/scripts/data_prep/count_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..8051655a0ec3ed00ddc3fb0835905a17b6dbd13a
--- /dev/null
+++ b/scripts/data_prep/count_dataset.py
@@ -0,0 +1,43 @@
+import os
+
+def count_images():
+ # Define the path to the dataset
+ # Based on your other scripts, it is in ../dataset/mission_dataset
+ base_dir = os.path.dirname(os.path.abspath(__file__))
+ dataset_dir = os.path.join(base_dir, '..', '..', '..', 'dataset', 'mission_dataset')
+
+ print(f"π Checking dataset at: {os.path.abspath(dataset_dir)}\n")
+
+ if not os.path.exists(dataset_dir):
+ print(f"β Error: Folder not found. Have you run 'organize_dataset.py'?")
+ return
+
+ total_images = 0
+
+ # Get all subfolders (classes)
+ try:
+ classes = [d for d in os.listdir(dataset_dir) if os.path.isdir(os.path.join(dataset_dir, d))]
+ classes.sort()
+ except Exception as e:
+ print(f"β Error reading directory: {e}")
+ return
+
+ print(f"{'CLASS NAME':<35} | {'COUNT':<10} | {'STATUS'}")
+ print("-" * 50)
+
+ for class_name in classes:
+ class_path = os.path.join(dataset_dir, class_name)
+ # Count files that look like images
+ images = [f for f in os.listdir(class_path) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp', '.bmp'))]
+ count = len(images)
+
+ status = "β Ready" if count >= 100 else "β οΈ Low Data" if count > 0 else "β Empty"
+
+ print(f"{class_name:<35} | {count:<10} | {status}")
+ total_images += count
+
+ print("-" * 50)
+ print(f"β TOTAL IMAGES: {total_images}")
+
+if __name__ == "__main__":
+ count_images()
\ No newline at end of file
diff --git a/scripts/data_prep/fix_dataset.py b/scripts/data_prep/fix_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..ffafa37ba4d47b68c37f2060a8e441757acf847c
--- /dev/null
+++ b/scripts/data_prep/fix_dataset.py
@@ -0,0 +1,28 @@
+import os
+import shutil
+
+# Define paths
+import os
+base_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', '..', 'dataset', 'garbage_classification')
+old_planting = os.path.join(base_dir, "planting")
+new_planting = os.path.join(base_dir, "SDG13_15_Planting")
+
+# Create new folder if it doesn't exist
+if not os.path.exists(new_planting):
+ os.makedirs(new_planting)
+
+# Move files from Old -> New
+if os.path.exists(old_planting):
+ print(f"π Moving files from '{old_planting}' to '{new_planting}'...")
+ files = os.listdir(old_planting)
+ for file in files:
+ old_path = os.path.join(old_planting, file)
+ new_path = os.path.join(new_planting, f"old_{file}") # Rename to avoid conflicts
+ shutil.move(old_path, new_path)
+
+ # Delete the empty old folder
+ os.rmdir(old_planting)
+ print("β Successfully merged folders!")
+ print("ποΈ Deleted old 'planting' folder.")
+else:
+ print("β οΈ Old 'planting' folder not found. Already merged?")
\ No newline at end of file
diff --git a/scripts/data_prep/organize_dataset.py b/scripts/data_prep/organize_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..17cc6f8aeb05c7c79c647359bcac8834e6c5234e
--- /dev/null
+++ b/scripts/data_prep/organize_dataset.py
@@ -0,0 +1,91 @@
+import os
+import shutil
+
+# π CONFIGURATION
+# Current script directory
+CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
+# The main project dataset folder (../dataset)
+BASE_DIR = os.path.join(CURRENT_DIR, '..', '..', '..', 'dataset')
+
+# 1. The "Correct" Destination
+FINAL_DEST = os.path.join(BASE_DIR, "mission_dataset")
+
+# 2. The "Old" Kaggle Dataset
+OLD_GARBAGE_DIR = os.path.join(BASE_DIR, "garbage_classification")
+
+# 3. The "Misplaced" Downloads (if any) inside mission17-ai/dataset
+MISPLACED_DIR = os.path.join(CURRENT_DIR, '..', '..', "dataset", "mission_dataset")
+
+# Map OLD folders to NEW SDG destinations
+# We are putting ALL waste items into SDG12 (Responsible Consumption & Production)
+MOVES = {
+ "SDG12_Recycling": [
+ "battery", "brown-glass", "cardboard",
+ "clothes", "green-glass", "metal", "paper",
+ "plastic", "shoes", "white-glass"
+ ],
+ "Non_SDG_Invalid": [
+ "trash", "biological"
+ ]
+}
+
+def organize_files():
+ print(f"π¦ Organizing dataset...")
+
+ # Ensure destination exists
+ if not os.path.exists(FINAL_DEST):
+ os.makedirs(FINAL_DEST)
+ print(f" β Created '{FINAL_DEST}'")
+
+ # --- STEP 1: Merge Kaggle Data ---
+ if os.path.exists(OLD_GARBAGE_DIR):
+ print(f" π Merging 'garbage_classification'...")
+ for dest_folder, source_folders in MOVES.items():
+ dest_path = os.path.join(FINAL_DEST, dest_folder)
+ if not os.path.exists(dest_path): os.makedirs(dest_path)
+
+ for folder in source_folders:
+ src_path = os.path.join(OLD_GARBAGE_DIR, folder)
+ if os.path.exists(src_path):
+ # Move files
+ for file in os.listdir(src_path):
+ try:
+ shutil.move(os.path.join(src_path, file), os.path.join(dest_path, f"{folder}_{file}"))
+ except Exception: pass
+ # Remove empty folder
+ try:
+ os.rmdir(src_path)
+ except: pass
+
+ # Try to remove root garbage dir
+ try: os.rmdir(OLD_GARBAGE_DIR)
+ except: pass
+ print(" β Kaggle data merged.")
+
+ # --- STEP 2: Fix Misplaced Downloads ---
+ if os.path.exists(MISPLACED_DIR):
+ print(f" β οΈ Found misplaced images in '{MISPLACED_DIR}'. Moving them...")
+ for category in os.listdir(MISPLACED_DIR):
+ src = os.path.join(MISPLACED_DIR, category)
+ dest = os.path.join(FINAL_DEST, category)
+
+ if os.path.isdir(src):
+ if not os.path.exists(dest): os.makedirs(dest)
+ for file in os.listdir(src):
+ try:
+ shutil.move(os.path.join(src, file), os.path.join(dest, file))
+ except: pass
+ try: os.rmdir(src)
+ except: pass
+
+ # Cleanup parent 'dataset' in mission17-ai if empty
+ try:
+ os.rmdir(MISPLACED_DIR)
+ os.rmdir(os.path.join(CURRENT_DIR, '..', '..', "dataset"))
+ except: pass
+ print(" β Misplaced images moved to correct folder.")
+
+ print(f"\n⨠SUCCESS! Dataset is ready at: {FINAL_DEST}")
+
+if __name__ == "__main__":
+ organize_files()
\ No newline at end of file
diff --git a/scripts/testing/split_dataset.py b/scripts/testing/split_dataset.py
new file mode 100644
index 0000000000000000000000000000000000000000..63682fddba1b1d034c66ee731d8d85f6ce87358f
--- /dev/null
+++ b/scripts/testing/split_dataset.py
@@ -0,0 +1,73 @@
+import os
+import shutil
+import random
+from pathlib import Path
+
+# --- CONFIGURATION ---
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+DATASET_DIR = os.path.join(BASE_DIR, '..', '..', '..', 'dataset', 'mission_dataset')
+OUTPUT_DIR = os.path.join(BASE_DIR, '..', '..', '..', 'dataset', 'mission_dataset_split')
+
+# Split ratios
+TRAIN_RATIO = 0.80
+TEST_RATIO = 0.20
+
+def split_dataset():
+ """
+ Randomly splits the dataset into train/ and test/ folders.
+ This prevents 'Data Leakage' so your evaluate_model.py tests on truly unseen images.
+ """
+ print(f"π Splitting dataset: {DATASET_DIR}")
+ print(f" Outputting to: {OUTPUT_DIR}")
+
+ if not os.path.exists(DATASET_DIR):
+ print(f"β ERROR: Dataset not found at {DATASET_DIR}")
+ return
+
+ # Create output directories
+ train_dir = os.path.join(OUTPUT_DIR, 'train')
+ test_dir = os.path.join(OUTPUT_DIR, 'test')
+
+ os.makedirs(train_dir, exist_ok=True)
+ os.makedirs(test_dir, exist_ok=True)
+
+ classes = [d for d in os.listdir(DATASET_DIR) if os.path.isdir(os.path.join(DATASET_DIR, d))]
+
+ total_moved = 0
+
+ for class_name in classes:
+ class_path = os.path.join(DATASET_DIR, class_name)
+ images = [f for f in os.listdir(class_path) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
+
+ # Shuffle images randomly
+ random.shuffle(images)
+
+ # Calculate split index
+ split_idx = int(len(images) * TRAIN_RATIO)
+
+ train_images = images[:split_idx]
+ test_images = images[split_idx:]
+
+ # Create class folders in train/ and test/
+ os.makedirs(os.path.join(train_dir, class_name), exist_ok=True)
+ os.makedirs(os.path.join(test_dir, class_name), exist_ok=True)
+
+ print(f"π [{class_name}] Total: {len(images)} -> Train: {len(train_images)}, Test: {len(test_images)}")
+
+ # Copy files
+ for img in train_images:
+ shutil.copy2(os.path.join(class_path, img), os.path.join(train_dir, class_name, img))
+ total_moved += 1
+
+ for img in test_images:
+ shutil.copy2(os.path.join(class_path, img), os.path.join(test_dir, class_name, img))
+ total_moved += 1
+
+ print("=" * 50)
+ print(f"β Dataset split complete! {total_moved} images processed.")
+ print(" Next steps:")
+ print(" 1. Check the new folder 'mission_dataset_split'")
+ print(" 2. Run train_ai_v2.py (which now points to this new folder)")
+
+if __name__ == '__main__':
+ split_dataset()
diff --git a/scripts/testing/test_duplicate.py b/scripts/testing/test_duplicate.py
new file mode 100644
index 0000000000000000000000000000000000000000..60e94f3f75aca8bb5a604fea401a8156ce885ac8
--- /dev/null
+++ b/scripts/testing/test_duplicate.py
@@ -0,0 +1,16 @@
+import requests
+
+url = "http://localhost:5000/predict"
+file_path = "debug_bias_mitigation.jpg"
+
+print("--- First Upload ---")
+with open(file_path, "rb") as f:
+ r = requests.post(url, files={"file": f})
+ print(f"Status: {r.status_code}")
+ print(f"Response: {r.text}")
+
+print("\n--- Second Upload ---")
+with open(file_path, "rb") as f:
+ r = requests.post(url, files={"file": f})
+ print(f"Status: {r.status_code}")
+ print(f"Response: {r.text}")
diff --git a/scripts/testing/test_predictor.py b/scripts/testing/test_predictor.py
new file mode 100644
index 0000000000000000000000000000000000000000..9ee066a47d5cfe1f42370ecd49d0f05b48b851bc
--- /dev/null
+++ b/scripts/testing/test_predictor.py
@@ -0,0 +1,27 @@
+import sys
+import os
+sys.path.append(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
+from utils.predictor import Predictor
+
+
+def test_ollama():
+ print("Testing Ollama Predictor...")
+ p = Predictor()
+ print(f"Model configured: {p.get_model_name()}")
+
+ # We will just see if we can reach the API
+ # Since we need an image, let's create a dummy 1x1 black pixel image
+ import io
+ from PIL import Image
+
+ img = Image.new('RGB', (10, 10), color = 'black')
+ img_byte_arr = io.BytesIO()
+ img.save(img_byte_arr, format='PNG')
+ img_bytes = img_byte_arr.getvalue()
+
+ print("Sending dummy image to Ollama...")
+ res = p.predict(img_bytes)
+ print("Response:", res)
+
+if __name__ == "__main__":
+ test_ollama()
diff --git a/scripts/testing/test_server.py b/scripts/testing/test_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/scripts/testing/test_upload.html b/scripts/testing/test_upload.html
new file mode 100644
index 0000000000000000000000000000000000000000..698137fe1c0b5910efa82948960245215e272b7f
--- /dev/null
+++ b/scripts/testing/test_upload.html
@@ -0,0 +1,61 @@
+
+
+
+
+ Mission 17 AI Scanner
+
+
+
+
+
π€ Mission 17 AI Scanner
+
Upload a photo to verify your mission!
+
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/training/download_pangasinan.py b/scripts/training/download_pangasinan.py
new file mode 100644
index 0000000000000000000000000000000000000000..24cf21e58dd18585caa1e0ca25b519a9f4922791
--- /dev/null
+++ b/scripts/training/download_pangasinan.py
@@ -0,0 +1,46 @@
+import json
+import os
+from datasets import load_dataset
+
+print("Downloading dataset leklek02/pangasinan...")
+ds = load_dataset("leklek02/pangasinan", split="train")
+
+print(f"Total rows downloaded: {len(ds)}")
+
+# 1. Create Few-Shot Examples (Top 100 to keep the prompt size reasonable)
+examples = []
+for i in range(min(100, len(ds))):
+ row = ds[i]
+ user_text = str(row['instruction'])
+ if row.get('input') and str(row['input']).strip():
+ user_text += "\n" + str(row['input'])
+
+ examples.append({
+ "User": user_text,
+ "Bot": str(row['output'])
+ })
+
+backend_path = r"c:\Users\Kurt Perez\mission17\mission17-backend\utils\pangasinan_examples.json"
+os.makedirs(os.path.dirname(backend_path), exist_ok=True)
+with open(backend_path, "w", encoding="utf-8") as f:
+ json.dump(examples, f, indent=2, ensure_ascii=False)
+print(f"Saved 100 examples to {backend_path} for immediate use in Chatbot.")
+
+# 2. Create Gemini Tuning JSONL (All rows for future Fine-Tuning)
+tuning_path = r"c:\Users\Kurt Perez\mission17\dataset\gemini_pangasinan_tuning.jsonl"
+os.makedirs(os.path.dirname(tuning_path), exist_ok=True)
+with open(tuning_path, "w", encoding="utf-8") as f:
+ for row in ds:
+ user_text = str(row['instruction'])
+ if row.get('input') and str(row['input']).strip():
+ user_text += "\n" + str(row['input'])
+
+ jsonl_obj = {
+ "contents": [
+ {"role": "user", "parts": [{"text": user_text}]},
+ {"role": "model", "parts": [{"text": str(row['output'])}]}
+ ]
+ }
+ f.write(json.dumps(jsonl_obj, ensure_ascii=False) + "\n")
+
+print(f"Saved full tuning dataset to {tuning_path} (Upload this to Google AI Studio to fine-tune Gemini!)")
diff --git a/scripts/training/evaluate_model.py b/scripts/training/evaluate_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..b69313c823ede335a93b9e74079b47ebebb9f83f
--- /dev/null
+++ b/scripts/training/evaluate_model.py
@@ -0,0 +1,57 @@
+import numpy as np
+import tensorflow as tf
+from tensorflow.keras.preprocessing.image import ImageDataGenerator
+from tensorflow.keras.applications.efficientnet import preprocess_input
+from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, confusion_matrix
+import matplotlib.pyplot as plt
+import seaborn as sns
+import os
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+print("β³ Loading AI Model...")
+# π Ensure this is your correct model name!
+model = tf.keras.models.load_model(os.path.join(BASE_DIR, '..', '..', 'mission_model.h5'))
+
+print("π Loading Test Dataset...")
+# π Pointing to the new TEST split folder
+test_dir = os.path.join(BASE_DIR, '..', '..', '..', 'dataset', 'mission_dataset_split', 'test')
+
+test_datagen = ImageDataGenerator(preprocessing_function=preprocess_input)
+test_generator = test_datagen.flow_from_directory(
+ test_dir,
+ target_size=(224, 224),
+ batch_size=32,
+ class_mode='categorical',
+ shuffle=False
+)
+
+print("π€ Running Predictions (This may take a minute)...")
+Y_pred = model.predict(test_generator)
+y_pred_classes = np.argmax(Y_pred, axis=1) # π FIXED: Grabs the top prediction per image
+y_true = test_generator.classes
+
+print("\n" + "="*50)
+print("π CAPSTONE AI PERFORMANCE METRICS π")
+print("="*50)
+
+# π FIXED: Added average='weighted' to handle all 10 classes correctly
+accuracy = accuracy_score(y_true, y_pred_classes)
+precision = precision_score(y_true, y_pred_classes, average='weighted', zero_division=0)
+recall = recall_score(y_true, y_pred_classes, average='weighted', zero_division=0)
+f1 = f1_score(y_true, y_pred_classes, average='weighted', zero_division=0)
+
+print(f"β Accuracy: {accuracy * 100:.2f}%")
+print(f"π― Precision: {precision * 100:.2f}%")
+print(f"π Recall: {recall * 100:.2f}%")
+print(f"βοΈ F1-Score: {f1 * 100:.2f}%")
+print("="*50)
+
+# Make the confusion matrix chart larger to fit 10 classes
+cm = confusion_matrix(y_true, y_pred_classes)
+plt.figure(figsize=(10, 8))
+sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
+plt.title('AI Confusion Matrix (10 Classes)')
+plt.ylabel('Actual Image Class')
+plt.xlabel('AI Prediction')
+plt.savefig(os.path.join(BASE_DIR, '..', '..', 'outputs', 'confusion_matrix.png'))
+print("\nπ Saved 'confusion_matrix.png' to your outputs folder. Put this in your presentation!")
\ No newline at end of file
diff --git a/scripts/training/train_ai.py b/scripts/training/train_ai.py
new file mode 100644
index 0000000000000000000000000000000000000000..68e43b7f22b3713e40c2e6af39f31c5eaf02a5b9
--- /dev/null
+++ b/scripts/training/train_ai.py
@@ -0,0 +1,209 @@
+import os
+import tensorflow as tf
+from tensorflow.keras.preprocessing.image import ImageDataGenerator
+from tensorflow.keras.applications import EfficientNetB0
+from tensorflow.keras.applications.efficientnet import preprocess_input
+from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout, BatchNormalization
+from tensorflow.keras.models import Model
+from tensorflow.keras.optimizers import Adam
+from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint
+
+# --- CONFIGURATION ---
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+DATASET_DIR = os.path.join(BASE_DIR, '..', '..', '..', 'dataset', 'mission_dataset')
+MODEL_SAVE_PATH = os.path.join(BASE_DIR, '..', '..', 'mission_model.h5')
+LABELS_SAVE_PATH = os.path.join(BASE_DIR, '..', '..', 'labels.txt')
+
+# Hyperparameters
+IMG_SIZE = (224, 224)
+BATCH_SIZE = 32
+EPOCHS_INITIAL = 25 # Phase 1: Train top layers only
+EPOCHS_FINETUNE = 15 # Phase 2: Fine-tune top base layers
+LR_INITIAL = 1e-3 # Higher LR for initial training
+LR_FINETUNE = 1e-5 # Much lower LR for fine-tuning (prevents forgetting)
+FINETUNE_FROM_LAYER = 150 # Unfreeze EfficientNetB0 from this layer onwards
+
+def build_generators():
+ """Create training and validation data generators with strong augmentation."""
+ print("πΈ Preparing Image Generators with Strong Augmentation...")
+
+ # π₯ EfficientNetB0 has its own internal preprocessing β do NOT use rescale=1./255!
+ # Using preprocess_input correctly scales raw 0-255 pixel values for EfficientNet.
+ train_datagen = ImageDataGenerator(
+ preprocessing_function=preprocess_input, # β EfficientNetB0-compatible
+ rotation_range=30,
+ width_shift_range=0.2,
+ height_shift_range=0.2,
+ horizontal_flip=True,
+ brightness_range=[0.7, 1.3],
+ zoom_range=0.2,
+ shear_range=0.1,
+ channel_shift_range=20.0,
+ fill_mode='nearest',
+ validation_split=0.2
+ )
+
+ # Validation: only preprocess_input, NO augmentation
+ val_datagen = ImageDataGenerator(
+ preprocessing_function=preprocess_input, # β Must match training
+ validation_split=0.2
+ )
+
+ train_generator = train_datagen.flow_from_directory(
+ DATASET_DIR,
+ target_size=IMG_SIZE,
+ batch_size=BATCH_SIZE,
+ class_mode='categorical',
+ subset='training',
+ shuffle=True
+ )
+
+ validation_generator = val_datagen.flow_from_directory(
+ DATASET_DIR,
+ target_size=IMG_SIZE,
+ batch_size=BATCH_SIZE,
+ class_mode='categorical',
+ subset='validation',
+ shuffle=False
+ )
+
+ return train_generator, validation_generator
+
+
+def build_model(num_classes):
+ """
+ Build model using EfficientNetB0 (more accurate than MobileNetV2).
+ Phase 1 starts with all base layers FROZEN β only top layers train first.
+ """
+ print("π§ Building Model (EfficientNetB0 β upgraded from MobileNetV2)...")
+
+ base_model = EfficientNetB0(
+ weights='imagenet',
+ include_top=False,
+ input_shape=IMG_SIZE + (3,)
+ )
+ base_model.trainable = False # Freeze all base layers for Phase 1
+
+ x = base_model.output
+ x = GlobalAveragePooling2D()(x)
+ x = BatchNormalization()(x) # β¨ NEW β stabilizes training
+ x = Dropout(0.3)(x) # was 0.2 β slightly stronger regularization
+ x = Dense(256, activation='relu')(x) # β¨ NEW β extra dense layer for richer features
+ x = Dropout(0.2)(x)
+ predictions = Dense(num_classes, activation='softmax')(x)
+
+ model = Model(inputs=base_model.input, outputs=predictions)
+ return model, base_model
+
+
+def get_callbacks(phase_name):
+ """Smart callbacks: stop early if no improvement, reduce LR on plateau."""
+ return [
+ EarlyStopping(
+ monitor='val_accuracy',
+ patience=5, # Stop if no improvement for 5 epochs
+ restore_best_weights=True,
+ verbose=1
+ ),
+ ReduceLROnPlateau(
+ monitor='val_loss',
+ factor=0.5, # Halve LR if stuck
+ patience=3,
+ min_lr=1e-7,
+ verbose=1
+ ),
+ ModelCheckpoint(
+ filepath=MODEL_SAVE_PATH,
+ monitor='val_accuracy',
+ save_best_only=True, # Always keep the best checkpoint
+ verbose=1
+ )
+ ]
+
+
+def train_brain():
+ print("π Initializing Mission 17 AI Training (Enhanced)...")
+
+ # 1. CHECK DATASET
+ if not os.path.exists(DATASET_DIR):
+ print(f"β ERROR: Dataset not found at {DATASET_DIR}")
+ return
+
+ # 2. BUILD GENERATORS
+ try:
+ train_generator, validation_generator = build_generators()
+ except Exception as e:
+ print(f"β Error loading data: {e}")
+ return
+
+ if train_generator.samples == 0:
+ print("β No images found! Check your dataset structure.")
+ return
+
+ # 3. SAVE LABELS
+ class_names = list(train_generator.class_indices.keys())
+ print(f"π·οΈ Classes Detected: {class_names}")
+ with open(LABELS_SAVE_PATH, 'w') as f:
+ for name in class_names:
+ f.write(name + '\n')
+ print(f"β Labels saved to {LABELS_SAVE_PATH}")
+
+ num_classes = len(class_names)
+
+ # 4. BUILD MODEL
+ model, base_model = build_model(num_classes)
+
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+ # PHASE 1: Train top layers only (fast)
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+ print("\n" + "="*50)
+ print("ποΈ PHASE 1: Training Top Layers (Base Frozen)")
+ print("="*50)
+
+ model.compile(
+ optimizer=Adam(learning_rate=LR_INITIAL),
+ loss='categorical_crossentropy',
+ metrics=['accuracy']
+ )
+
+ model.fit(
+ train_generator,
+ epochs=EPOCHS_INITIAL,
+ validation_data=validation_generator,
+ callbacks=get_callbacks('phase1')
+ )
+
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+ # PHASE 2: Fine-tune top layers of base model
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+ print("\n" + "="*50)
+ print("π¬ PHASE 2: Fine-Tuning Top Base Layers")
+ print(f" Unfreezing EfficientNetB0 from layer {FINETUNE_FROM_LAYER}+")
+ print("="*50)
+
+ base_model.trainable = True
+
+ # Only unfreeze layers AFTER FINETUNE_FROM_LAYER β keep earlier layers frozen
+ for layer in base_model.layers[:FINETUNE_FROM_LAYER]:
+ layer.trainable = False
+
+ # CRITICAL: Recompile with much lower LR to avoid destroying pre-trained weights
+ model.compile(
+ optimizer=Adam(learning_rate=LR_FINETUNE),
+ loss='categorical_crossentropy',
+ metrics=['accuracy']
+ )
+
+ model.fit(
+ train_generator,
+ epochs=EPOCHS_FINETUNE,
+ validation_data=validation_generator,
+ callbacks=get_callbacks('phase2')
+ )
+
+ print(f"\nβ Training complete! Best model saved to {MODEL_SAVE_PATH}")
+ print(" Run evaluate_model.py to check accuracy metrics & confusion matrix.")
+
+
+if __name__ == '__main__':
+ train_brain()
\ No newline at end of file
diff --git a/scripts/training/train_ai_v2.py b/scripts/training/train_ai_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..c52ccd4fec2f813b44cffb09e543a4a56803ee17
--- /dev/null
+++ b/scripts/training/train_ai_v2.py
@@ -0,0 +1,170 @@
+import os
+import numpy as np
+import tensorflow as tf
+from tensorflow.keras.preprocessing.image import ImageDataGenerator
+from tensorflow.keras.applications import EfficientNetB0
+from tensorflow.keras.applications.efficientnet import preprocess_input
+from tensorflow.keras.layers import Dense, GlobalAveragePooling2D, Dropout, BatchNormalization
+from tensorflow.keras.models import Model
+from tensorflow.keras.optimizers import Adam
+from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau, ModelCheckpoint
+from sklearn.utils.class_weight import compute_class_weight
+
+# --- CONFIGURATION ---
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+# IMPORTANT: Pointing to the new split dataset folder
+DATASET_DIR = os.path.join(BASE_DIR, '..', '..', '..', 'dataset', 'mission_dataset_split', 'train')
+MODEL_SAVE_PATH = os.path.join(BASE_DIR, '..', '..', 'mission_model.h5')
+LABELS_SAVE_PATH = os.path.join(BASE_DIR, '..', '..', 'labels.txt')
+
+IMG_SIZE = (224, 224)
+BATCH_SIZE = 32
+EPOCHS_INITIAL = 20
+EPOCHS_FINETUNE = 15
+LR_INITIAL = 1e-3
+LR_FINETUNE = 1e-5
+
+def build_generators():
+ print("πΈ Preparing Image Generators...")
+
+ train_datagen = ImageDataGenerator(
+ preprocessing_function=preprocess_input,
+ rotation_range=30,
+ width_shift_range=0.2,
+ height_shift_range=0.2,
+ horizontal_flip=True,
+ brightness_range=[0.7, 1.3],
+ zoom_range=0.2,
+ validation_split=0.2 # 20% of the train/ folder becomes validation
+ )
+
+ train_generator = train_datagen.flow_from_directory(
+ DATASET_DIR,
+ target_size=IMG_SIZE,
+ batch_size=BATCH_SIZE,
+ class_mode='categorical',
+ subset='training',
+ shuffle=True
+ )
+
+ validation_generator = train_datagen.flow_from_directory(
+ DATASET_DIR,
+ target_size=IMG_SIZE,
+ batch_size=BATCH_SIZE,
+ class_mode='categorical',
+ subset='validation',
+ shuffle=False
+ )
+
+ return train_generator, validation_generator
+
+def get_class_weights(train_generator):
+ """
+ Calculates class weights to handle imbalanced datasets.
+ This stops the AI from being biased toward the majority class.
+ """
+ print("βοΈ Calculating Class Weights for balanced training...")
+ class_indices = train_generator.class_indices
+ classes = train_generator.classes
+
+ weights = compute_class_weight(
+ class_weight='balanced',
+ classes=np.unique(classes),
+ y=classes
+ )
+ class_weights = dict(enumerate(weights))
+
+ print(" Weights applied:")
+ for cls_name, cls_idx in class_indices.items():
+ print(f" - {cls_name}: {class_weights[cls_idx]:.2f}")
+
+ return class_weights
+
+def build_model(num_classes):
+ print("π§ Building Model (EfficientNetB0)...")
+
+ base_model = EfficientNetB0(
+ weights='imagenet',
+ include_top=False,
+ input_shape=IMG_SIZE + (3,)
+ )
+ base_model.trainable = False
+
+ x = base_model.output
+ x = GlobalAveragePooling2D()(x)
+ x = BatchNormalization()(x)
+ x = Dropout(0.3)(x)
+ x = Dense(256, activation='relu')(x)
+ x = BatchNormalization()(x)
+ x = Dropout(0.3)(x)
+ predictions = Dense(num_classes, activation='softmax')(x)
+
+ model = Model(inputs=base_model.input, outputs=predictions)
+ return model, base_model
+
+def get_callbacks(phase_name):
+ return [
+ EarlyStopping(monitor='val_accuracy', patience=5, restore_best_weights=True, verbose=1),
+ ReduceLROnPlateau(monitor='val_loss', factor=0.5, patience=3, min_lr=1e-7, verbose=1),
+ ModelCheckpoint(filepath=MODEL_SAVE_PATH, monitor='val_accuracy', save_best_only=True, verbose=1)
+ ]
+
+def train_brain():
+ print("π Initializing Mission 17 AI Training v2 (Optimized)...")
+
+ if not os.path.exists(DATASET_DIR):
+ print(f"β ERROR: Training Dataset not found at {DATASET_DIR}")
+ print(" Did you run scripts/testing/split_dataset.py first?")
+ return
+
+ train_generator, validation_generator = build_generators()
+
+ # Save Labels
+ class_names = list(train_generator.class_indices.keys())
+ with open(LABELS_SAVE_PATH, 'w') as f:
+ for name in class_names:
+ f.write(name + '\n')
+
+ num_classes = len(class_names)
+
+ # Get Class Weights
+ class_weights = get_class_weights(train_generator)
+
+ model, base_model = build_model(num_classes)
+
+ # --- PHASE 1 ---
+ print("\n" + "="*50)
+ print("ποΈ PHASE 1: Training Top Layers (Base Frozen)")
+ print("="*50)
+
+ model.compile(optimizer=Adam(learning_rate=LR_INITIAL), loss='categorical_crossentropy', metrics=['accuracy'])
+ model.fit(
+ train_generator,
+ epochs=EPOCHS_INITIAL,
+ validation_data=validation_generator,
+ class_weight=class_weights, # Apply weights!
+ callbacks=get_callbacks('phase1')
+ )
+
+ # --- PHASE 2 ---
+ print("\n" + "="*50)
+ print("π¬ PHASE 2: Fine-Tuning Top Base Layers")
+ print("="*50)
+
+ base_model.trainable = True
+ for layer in base_model.layers[:150]:
+ layer.trainable = False
+
+ model.compile(optimizer=Adam(learning_rate=LR_FINETUNE), loss='categorical_crossentropy', metrics=['accuracy'])
+ model.fit(
+ train_generator,
+ epochs=EPOCHS_FINETUNE,
+ validation_data=validation_generator,
+ class_weight=class_weights, # Apply weights!
+ callbacks=get_callbacks('phase2')
+ )
+
+ print(f"\nβ Training complete! Model saved to {MODEL_SAVE_PATH}")
+
+if __name__ == '__main__':
+ train_brain()
diff --git a/utils/__init__.py b/utils/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..da8e0b08bf26910d184ce402279af8012c4e6ccf
--- /dev/null
+++ b/utils/__init__.py
@@ -0,0 +1 @@
+"""Utils module for Mission 17 AI."""
diff --git a/utils/__pycache__/__init__.cpython-311.pyc b/utils/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..88259ca104aac780fd91280548813c1a1422f92b
Binary files /dev/null and b/utils/__pycache__/__init__.cpython-311.pyc differ
diff --git a/utils/__pycache__/anticheat.cpython-311.pyc b/utils/__pycache__/anticheat.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..6489e5e1ffe678fc09615fbda9fd26e3606593a0
Binary files /dev/null and b/utils/__pycache__/anticheat.cpython-311.pyc differ
diff --git a/utils/__pycache__/predictor.cpython-311.pyc b/utils/__pycache__/predictor.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e889f8c6dc15dfea5f1d077710d30e8fef688964
Binary files /dev/null and b/utils/__pycache__/predictor.cpython-311.pyc differ
diff --git a/utils/__pycache__/verdict.cpython-311.pyc b/utils/__pycache__/verdict.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..e1175886425317c7d49e6be18962cb7cd5b006de
Binary files /dev/null and b/utils/__pycache__/verdict.cpython-311.pyc differ
diff --git a/utils/anticheat.py b/utils/anticheat.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0953f5f1eec34fa5898897559af63b240aa45fd
--- /dev/null
+++ b/utils/anticheat.py
@@ -0,0 +1,98 @@
+import os
+import json
+import io
+import imagehash
+from PIL import Image
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+HASH_FILE = os.path.join(BASE_DIR, 'anticheat_hashes.json')
+
+class AntiCheatEngine:
+ def __init__(self):
+ self.hashes = set()
+ self._load_hashes()
+
+ def _load_hashes(self):
+ if os.path.exists(HASH_FILE):
+ try:
+ with open(HASH_FILE, 'r') as f:
+ data = json.load(f)
+ self.hashes = set(data.get("hashes", []))
+ except Exception as e:
+ print(f"β οΈ Could not load anticheat hashes: {e}")
+
+ def _save_hashes(self):
+ try:
+ with open(HASH_FILE, 'w') as f:
+ json.dump({"hashes": list(self.hashes)}, f)
+ except Exception as e:
+ print(f"β οΈ Could not save anticheat hashes: {e}")
+
+ def get_hashes(self, file_bytes):
+ """Calculates pHash and dHash for better duplicate detection."""
+ try:
+ img = Image.open(io.BytesIO(file_bytes)).convert('RGB')
+ p_hash = str(imagehash.phash(img))
+ d_hash = str(imagehash.dhash(img))
+ return p_hash, d_hash
+ except Exception:
+ return None, None
+
+ def is_duplicate(self, file_bytes, similarity_threshold=8):
+ """
+ Checks if the image is a duplicate based on stored hashes.
+ similarity_threshold: the max hamming distance to be considered a duplicate.
+ """
+ p_hash_str, d_hash_str = self.get_hashes(file_bytes)
+
+ if not p_hash_str or not d_hash_str:
+ return False
+
+ # Check exact matches first for speed
+ if p_hash_str in self.hashes or d_hash_str in self.hashes:
+ return True
+
+ p_hash = imagehash.hex_to_hash(p_hash_str)
+ d_hash = imagehash.hex_to_hash(d_hash_str)
+
+ # Check similarity (hamming distance)
+ for stored_hash_str in self.hashes:
+ try:
+ stored_hash = imagehash.hex_to_hash(stored_hash_str)
+ # Compare both pHash and dHash representation lengths isn't an issue since they are stored as strings
+ # but we should compare apples to apples. Let's simplify and just do exact match on dHash and pHash,
+ # but also check similarity if we parse them properly.
+
+ # For safety, let's just do an exact match on string representations for now,
+ # or a simple distance check if we assume all stored are pHashes.
+ # Since we store both, some might be dHash, some pHash.
+ # Let's just compare distances safely.
+ distance = p_hash - stored_hash
+ if distance < similarity_threshold:
+ return True
+
+ distance = d_hash - stored_hash
+ if distance < similarity_threshold:
+ return True
+ except Exception:
+ continue
+
+ return False
+
+ def register(self, file_bytes):
+ """Registers a new image hash to prevent future duplicates."""
+ p_hash_str, d_hash_str = self.get_hashes(file_bytes)
+ if p_hash_str:
+ self.hashes.add(p_hash_str)
+ if d_hash_str:
+ self.hashes.add(d_hash_str)
+ self._save_hashes()
+ return p_hash_str
+
+ def clear(self):
+ self.hashes.clear()
+ self._save_hashes()
+ return len(self.hashes)
+
+ def count(self):
+ return len(self.hashes)
diff --git a/utils/predictor.py b/utils/predictor.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce011d50c356491fc5df39965cede27936f0deec
--- /dev/null
+++ b/utils/predictor.py
@@ -0,0 +1,109 @@
+import os
+import io
+import traceback
+import numpy as np
+from PIL import Image
+from tensorflow.keras.models import load_model
+from tensorflow.keras.applications.efficientnet import preprocess_input
+
+BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+MODEL_PATH = os.path.join(BASE_DIR, 'mission_model.h5')
+LABELS_PATH = os.path.join(BASE_DIR, 'labels.txt')
+
+class Predictor:
+ def __init__(self):
+ self.model = None
+ self.class_names = []
+ self._load_model()
+
+ def _load_model(self):
+ print("π§ Loading TensorFlow CNN Brain...")
+ if not os.path.exists(MODEL_PATH):
+ print(f"β ERROR: {MODEL_PATH} not found. You need to train the model!")
+ return
+
+ try:
+ self.model = load_model(MODEL_PATH)
+ print("β Model loaded successfully!")
+ except Exception as e:
+ print(f"β Failed to load model: {e}")
+
+ # Load Labels
+ try:
+ with open(LABELS_PATH, 'r') as f:
+ self.class_names = [line.strip() for line in f.readlines()]
+ print(f"π·οΈ Labels loaded: {self.class_names}")
+ except FileNotFoundError:
+ print("β ERROR: labels.txt not found.")
+ self.class_names = []
+
+ # π₯ WARMUP STEP (Optimization)
+ if self.model:
+ print("π₯ Warming up model for instant first-prediction...")
+ dummy_image = np.zeros((1, 224, 224, 3), dtype=np.float32)
+ self.model.predict(dummy_image, verbose=0)
+ print("β‘ AI is fully optimized and ready!")
+
+ def predict(self, file_bytes):
+ """
+ Runs the image through the custom EfficientNet CNN.
+ """
+ if not self.model or not self.class_names:
+ return {"category": "Non_SDG_Invalid", "confidence": 0, "reason": "Model offline or missing."}
+
+ try:
+ # 1. Read image using PIL (just like in train_ai.py)
+ img = Image.open(io.BytesIO(file_bytes)).convert('RGB')
+
+ # 2. Resize to 224x224 (EfficientNetB0 input size)
+ img = img.resize((224, 224), Image.LANCZOS)
+
+ # 3. Apply EfficientNetB0 preprocess_input
+ img_array = np.array(img, dtype=np.float32)
+ img_array = preprocess_input(img_array)
+ img_array = np.expand_dims(img_array, axis=0)
+
+ # 4. Predict
+ predictions = self.model.predict(img_array)
+ score = predictions[0]
+
+ top_index = np.argmax(score)
+ label = self.class_names[top_index]
+
+ confidence = int(np.max(score) * 100)
+
+ # Clean up label if it has the SDG prefix (e.g. SDG12_Recycling -> Recycling)
+ # The verdict.py MISSION_MAP expects "Recycling", "Planting", etc.
+ category = label
+ if "_" in label and label.startswith("SDG"):
+ # E.g. "SDG12_Recycling" -> "Recycling"
+ category = label.split("_", 1)[1]
+ # If there are multiple underscores (like SDG13_15_Planting), take the last part
+ if "_" in category:
+ category = category.rsplit("_", 1)[-1]
+ elif label == "Non_SDG_Invalid":
+ category = "Non_SDG_Invalid"
+
+ # Quick check for combined strings
+ if "Planting" in label: category = "Planting"
+ if "Cleanup" in label: category = "Cleanup"
+ if "Donation" in label: category = "Donation"
+ if "Cities" in label or "Sustainable" in label: category = "Sustainable_Cities"
+ if "Local" in label: category = "Support_Local"
+ if "Health" in label: category = "Health"
+ if "Energy" in label: category = "Energy"
+ if "Education" in label: category = "Education"
+
+ return {
+ "category": category,
+ "confidence": confidence,
+ "reason": f"Predicted {label} with {confidence}% confidence"
+ }
+
+ except Exception as e:
+ traceback.print_exc()
+ print(f"β οΈ Predictor error: {e}")
+ return {"category": "Non_SDG_Invalid", "confidence": 0, "reason": str(e)}
+
+ def get_model_name(self):
+ return "Custom CNN (mission_model.h5)"
diff --git a/utils/verdict.py b/utils/verdict.py
new file mode 100644
index 0000000000000000000000000000000000000000..370df1dbbaf19f7dac2796adec3a72a580009a55
--- /dev/null
+++ b/utils/verdict.py
@@ -0,0 +1,42 @@
+# Maps AI prediction to (Verdict, Message, SDG)
+MISSION_MAP = {
+ "Planting": ("VERIFIED", "β Valid Planting Mission (SDG 13/15)", "SDG 13/15"),
+ "Recycling": ("VERIFIED", "β Valid Recycling Mission (SDG 12)", "SDG 12"),
+ "Cleanup": ("VERIFIED", "β Valid Cleanup Mission (SDG 6/14)", "SDG 6/14"),
+ "Donation": ("VERIFIED", "β Valid Donation Mission (SDG 1/2)", "SDG 1/2"),
+ "Health": ("VERIFIED", "β Valid Health & Wellness (SDG 3)", "SDG 3"),
+ "Education": ("VERIFIED", "β Valid Education Activity (SDG 4)", "SDG 4"),
+ "Energy": ("VERIFIED", "β Valid Energy Saving Action (SDG 7)", "SDG 7"),
+ "Sustainable_Cities": ("VERIFIED", "β Valid Sustainable Commute (SDG 11)", "SDG 11"),
+ "Support_Local": ("VERIFIED", "β Valid Support for Local Biz (SDG 8)", "SDG 8"),
+ "Non_SDG_Invalid": ("REJECTED", "β οΈ Image does not match any mission.", "N/A"),
+}
+
+def get_verdict(category, confidence_percent, threshold=55):
+ """
+ Returns the final verdict response dictionary.
+ Requires a confidence of at least `threshold` for a VERIFIED verdict.
+ """
+ verdict, message, sdg = MISSION_MAP.get(category, ("REJECTED", "β οΈ Unknown Image Category.", "N/A"))
+
+ is_verified = (verdict == "VERIFIED")
+
+ # If it's technically a valid category but confidence is too low
+ if is_verified and confidence_percent < threshold:
+ verdict = "UNCERTAIN"
+ message = f"β Unclear Image ({confidence_percent}%). Please take a clearer photo."
+ is_verified = False
+ sdg = "N/A"
+
+ source_check = "πΈ Raw Picture" if is_verified else "π€ AI Generated / Invalid"
+
+ return {
+ 'prediction': category,
+ 'confidence': f"{confidence_percent}%",
+ 'confidence_raw': confidence_percent,
+ 'verdict': verdict,
+ 'message': message,
+ 'is_verified': is_verified,
+ 'sdg': sdg,
+ 'source_check': source_check
+ }
diff --git a/venv/.gitignore b/venv/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..49b53dcfd66bcbbee1f9f854a995842a8263d3d3
--- /dev/null
+++ b/venv/.gitignore
@@ -0,0 +1,2 @@
+# Created by venv; see https://docs.python.org/3/library/venv.html
+*
diff --git a/venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc b/venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ebffadbd743eff9b2a92c659fe5af0f0ea89001a
Binary files /dev/null and b/venv/Lib/site-packages/__pycache__/pylab.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/__pycache__/six.cpython-311.pyc b/venv/Lib/site-packages/__pycache__/six.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0bf8681c6a2a4cb7a20e8a736b6b06209d0f690f
Binary files /dev/null and b/venv/Lib/site-packages/__pycache__/six.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/__pycache__/threadpoolctl.cpython-311.pyc b/venv/Lib/site-packages/__pycache__/threadpoolctl.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1146d5a8c9d23ec87a1dbcce10ad71333fcae863
Binary files /dev/null and b/venv/Lib/site-packages/__pycache__/threadpoolctl.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/__pycache__/typing_extensions.cpython-311.pyc b/venv/Lib/site-packages/__pycache__/typing_extensions.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..3bce6209eebf8912e94ae508e36b043372c43e62
--- /dev/null
+++ b/venv/Lib/site-packages/__pycache__/typing_extensions.cpython-311.pyc
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:8579e0349d3763ed1ffe4345807900a184ffc9f6dacf47f1b52f7d8104f1640b
+size 179469
diff --git a/venv/Lib/site-packages/_distutils_hack/__init__.py b/venv/Lib/site-packages/_distutils_hack/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..f987a5367fdfaa4f17cd4bf700d56f4b50992368
--- /dev/null
+++ b/venv/Lib/site-packages/_distutils_hack/__init__.py
@@ -0,0 +1,222 @@
+# don't import any costly modules
+import sys
+import os
+
+
+is_pypy = '__pypy__' in sys.builtin_module_names
+
+
+def warn_distutils_present():
+ if 'distutils' not in sys.modules:
+ return
+ if is_pypy and sys.version_info < (3, 7):
+ # PyPy for 3.6 unconditionally imports distutils, so bypass the warning
+ # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250
+ return
+ import warnings
+
+ warnings.warn(
+ "Distutils was imported before Setuptools, but importing Setuptools "
+ "also replaces the `distutils` module in `sys.modules`. This may lead "
+ "to undesirable behaviors or errors. To avoid these issues, avoid "
+ "using distutils directly, ensure that setuptools is installed in the "
+ "traditional way (e.g. not an editable install), and/or make sure "
+ "that setuptools is always imported before distutils."
+ )
+
+
+def clear_distutils():
+ if 'distutils' not in sys.modules:
+ return
+ import warnings
+
+ warnings.warn("Setuptools is replacing distutils.")
+ mods = [
+ name
+ for name in sys.modules
+ if name == "distutils" or name.startswith("distutils.")
+ ]
+ for name in mods:
+ del sys.modules[name]
+
+
+def enabled():
+ """
+ Allow selection of distutils by environment variable.
+ """
+ which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local')
+ return which == 'local'
+
+
+def ensure_local_distutils():
+ import importlib
+
+ clear_distutils()
+
+ # With the DistutilsMetaFinder in place,
+ # perform an import to cause distutils to be
+ # loaded from setuptools._distutils. Ref #2906.
+ with shim():
+ importlib.import_module('distutils')
+
+ # check that submodules load as expected
+ core = importlib.import_module('distutils.core')
+ assert '_distutils' in core.__file__, core.__file__
+ assert 'setuptools._distutils.log' not in sys.modules
+
+
+def do_override():
+ """
+ Ensure that the local copy of distutils is preferred over stdlib.
+
+ See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401
+ for more motivation.
+ """
+ if enabled():
+ warn_distutils_present()
+ ensure_local_distutils()
+
+
+class _TrivialRe:
+ def __init__(self, *patterns):
+ self._patterns = patterns
+
+ def match(self, string):
+ return all(pat in string for pat in self._patterns)
+
+
+class DistutilsMetaFinder:
+ def find_spec(self, fullname, path, target=None):
+ # optimization: only consider top level modules and those
+ # found in the CPython test suite.
+ if path is not None and not fullname.startswith('test.'):
+ return
+
+ method_name = 'spec_for_{fullname}'.format(**locals())
+ method = getattr(self, method_name, lambda: None)
+ return method()
+
+ def spec_for_distutils(self):
+ if self.is_cpython():
+ return
+
+ import importlib
+ import importlib.abc
+ import importlib.util
+
+ try:
+ mod = importlib.import_module('setuptools._distutils')
+ except Exception:
+ # There are a couple of cases where setuptools._distutils
+ # may not be present:
+ # - An older Setuptools without a local distutils is
+ # taking precedence. Ref #2957.
+ # - Path manipulation during sitecustomize removes
+ # setuptools from the path but only after the hook
+ # has been loaded. Ref #2980.
+ # In either case, fall back to stdlib behavior.
+ return
+
+ class DistutilsLoader(importlib.abc.Loader):
+ def create_module(self, spec):
+ mod.__name__ = 'distutils'
+ return mod
+
+ def exec_module(self, module):
+ pass
+
+ return importlib.util.spec_from_loader(
+ 'distutils', DistutilsLoader(), origin=mod.__file__
+ )
+
+ @staticmethod
+ def is_cpython():
+ """
+ Suppress supplying distutils for CPython (build and tests).
+ Ref #2965 and #3007.
+ """
+ return os.path.isfile('pybuilddir.txt')
+
+ def spec_for_pip(self):
+ """
+ Ensure stdlib distutils when running under pip.
+ See pypa/pip#8761 for rationale.
+ """
+ if self.pip_imported_during_build():
+ return
+ clear_distutils()
+ self.spec_for_distutils = lambda: None
+
+ @classmethod
+ def pip_imported_during_build(cls):
+ """
+ Detect if pip is being imported in a build script. Ref #2355.
+ """
+ import traceback
+
+ return any(
+ cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None)
+ )
+
+ @staticmethod
+ def frame_file_is_setup(frame):
+ """
+ Return True if the indicated frame suggests a setup.py file.
+ """
+ # some frames may not have __file__ (#2940)
+ return frame.f_globals.get('__file__', '').endswith('setup.py')
+
+ def spec_for_sensitive_tests(self):
+ """
+ Ensure stdlib distutils when running select tests under CPython.
+
+ python/cpython#91169
+ """
+ clear_distutils()
+ self.spec_for_distutils = lambda: None
+
+ sensitive_tests = (
+ [
+ 'test.test_distutils',
+ 'test.test_peg_generator',
+ 'test.test_importlib',
+ ]
+ if sys.version_info < (3, 10)
+ else [
+ 'test.test_distutils',
+ ]
+ )
+
+
+for name in DistutilsMetaFinder.sensitive_tests:
+ setattr(
+ DistutilsMetaFinder,
+ f'spec_for_{name}',
+ DistutilsMetaFinder.spec_for_sensitive_tests,
+ )
+
+
+DISTUTILS_FINDER = DistutilsMetaFinder()
+
+
+def add_shim():
+ DISTUTILS_FINDER in sys.meta_path or insert_shim()
+
+
+class shim:
+ def __enter__(self):
+ insert_shim()
+
+ def __exit__(self, exc, value, tb):
+ remove_shim()
+
+
+def insert_shim():
+ sys.meta_path.insert(0, DISTUTILS_FINDER)
+
+
+def remove_shim():
+ try:
+ sys.meta_path.remove(DISTUTILS_FINDER)
+ except ValueError:
+ pass
diff --git a/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..1727c90b13a8637df8ba7ed09b39bc9daec02ebe
Binary files /dev/null and b/venv/Lib/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc b/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..559a1ee993d22825ccae4af060a4c6a5a0719a12
Binary files /dev/null and b/venv/Lib/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/_distutils_hack/override.py b/venv/Lib/site-packages/_distutils_hack/override.py
new file mode 100644
index 0000000000000000000000000000000000000000..2cc433a4a55e3b41fa31089918fb62096092f89f
--- /dev/null
+++ b/venv/Lib/site-packages/_distutils_hack/override.py
@@ -0,0 +1 @@
+__import__('_distutils_hack').do_override()
diff --git a/venv/Lib/site-packages/_multiprocess/__init__.py b/venv/Lib/site-packages/_multiprocess/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..9a89f71736472fe74c8f3a5ffac26b9bb8dcc9a2
--- /dev/null
+++ b/venv/Lib/site-packages/_multiprocess/__init__.py
@@ -0,0 +1,8 @@
+#!/usr/bin/env python
+#
+# Author: Mike McKerns (mmckerns @caltech and @uqfoundation)
+# Copyright (c) 2022-2026 The Uncertainty Quantification Foundation.
+# License: 3-clause BSD. The full license text is available at:
+# - https://github.com/uqfoundation/multiprocess/blob/master/LICENSE
+
+from _multiprocessing import *
diff --git a/venv/Lib/site-packages/_multiprocess/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/_multiprocess/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..075cb1e0936c01a233d527f6378567cf36a6ac0c
Binary files /dev/null and b/venv/Lib/site-packages/_multiprocess/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/_yaml/__init__.py b/venv/Lib/site-packages/_yaml/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7baa8c4b68127d5cdf0be9a799429e61347c2694
--- /dev/null
+++ b/venv/Lib/site-packages/_yaml/__init__.py
@@ -0,0 +1,33 @@
+# This is a stub package designed to roughly emulate the _yaml
+# extension module, which previously existed as a standalone module
+# and has been moved into the `yaml` package namespace.
+# It does not perfectly mimic its old counterpart, but should get
+# close enough for anyone who's relying on it even when they shouldn't.
+import yaml
+
+# in some circumstances, the yaml module we imoprted may be from a different version, so we need
+# to tread carefully when poking at it here (it may not have the attributes we expect)
+if not getattr(yaml, '__with_libyaml__', False):
+ from sys import version_info
+
+ exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError
+ raise exc("No module named '_yaml'")
+else:
+ from yaml._yaml import *
+ import warnings
+ warnings.warn(
+ 'The _yaml extension module is now located at yaml._yaml'
+ ' and its location is subject to change. To use the'
+ ' LibYAML-based parser and emitter, import from `yaml`:'
+ ' `from yaml import CLoader as Loader, CDumper as Dumper`.',
+ DeprecationWarning
+ )
+ del warnings
+ # Don't `del yaml` here because yaml is actually an existing
+ # namespace member of _yaml.
+
+__name__ = '_yaml'
+# If the module is top-level (i.e. not a part of any specific package)
+# then the attribute should be set to ''.
+# https://docs.python.org/3.8/library/types.html
+__package__ = ''
diff --git a/venv/Lib/site-packages/_yaml/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/_yaml/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b4f51ef92fabe8ed032a61aa01cd1e19158e58fc
Binary files /dev/null and b/venv/Lib/site-packages/_yaml/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/__init__.py b/venv/Lib/site-packages/absl/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..341e11f037891aa17ddeb7f0bfcac781d321daa6
--- /dev/null
+++ b/venv/Lib/site-packages/absl/__init__.py
@@ -0,0 +1,15 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+__version__ = '2.4.0'
diff --git a/venv/Lib/site-packages/absl/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/absl/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..78480e58608897b12abacf92222f13b1c85e60c7
Binary files /dev/null and b/venv/Lib/site-packages/absl/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/__pycache__/app.cpython-311.pyc b/venv/Lib/site-packages/absl/__pycache__/app.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..37ef862f7e74d18fdf9589640f1ebd9840ee2028
Binary files /dev/null and b/venv/Lib/site-packages/absl/__pycache__/app.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/__pycache__/command_name.cpython-311.pyc b/venv/Lib/site-packages/absl/__pycache__/command_name.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..5e08a53eeac9d8eb51eeb506fc2b9c7486aee41c
Binary files /dev/null and b/venv/Lib/site-packages/absl/__pycache__/command_name.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/app.py b/venv/Lib/site-packages/absl/app.py
new file mode 100644
index 0000000000000000000000000000000000000000..4656f96c2fefced69c13d87084c0049813b20e49
--- /dev/null
+++ b/venv/Lib/site-packages/absl/app.py
@@ -0,0 +1,539 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Generic entry point for Abseil Python applications.
+
+To use this module, define a ``main`` function with a single ``argv`` argument
+and call ``app.run(main)``. For example::
+
+ def main(argv):
+ if len(argv) > 1:
+ raise app.UsageError('Too many command-line arguments.')
+
+ if __name__ == '__main__':
+ app.run(main)
+"""
+
+import collections
+import errno
+import importlib
+import os
+import pdb
+import sys
+import textwrap
+import traceback
+
+from absl import command_name
+from absl import flags
+from absl import logging
+
+try:
+ import faulthandler
+except ImportError:
+ faulthandler = None
+
+FLAGS = flags.FLAGS
+
+flags.DEFINE_boolean(
+ 'run_with_pdb',
+ False,
+ 'Set to true for debug mode. PDB is used by default; $PYTHONBREAKPOINT '
+ '(https://docs.python.org/3/using/cmdline.html#envvar-PYTHONBREAKPOINT) '
+ 'can be used to specify a custom debugger.',
+)
+flags.DEFINE_boolean(
+ 'pdb_post_mortem',
+ False,
+ 'Set to true to handle uncaught exceptions with the post mortem debugger. '
+ 'PDB is used by default; $PYTHONBREAKPOINT '
+ '(https://docs.python.org/3/using/cmdline.html#envvar-PYTHONBREAKPOINT) '
+ 'can be used to specify a custom one.',
+)
+flags.DEFINE_alias('pdb', 'pdb_post_mortem')
+flags.DEFINE_boolean('run_with_profiling', False,
+ 'Set to true for profiling the script. '
+ 'Execution will be slower, and the output format might '
+ 'change over time.')
+flags.DEFINE_string('profile_file', None,
+ 'Dump profile information to a file (for python -m '
+ 'pstats). Implies --run_with_profiling.')
+flags.DEFINE_boolean('use_cprofile_for_profiling', True,
+ 'Use cProfile instead of the profile module for '
+ 'profiling. This has no effect unless '
+ '--run_with_profiling is set.')
+flags.DEFINE_boolean('only_check_args', False,
+ 'Set to true to validate args and exit.',
+ allow_hide_cpp=True)
+
+
+def _get_debugger_module_with_function(function_name):
+ """Provides the `$PYTHONBREAKPOINT` module if it contains `function_name`.
+
+ Falls back to `pdb` otherwise.
+
+ Args:
+ function_name: The name of the function required.
+
+ Returns:
+ A debugger module providing `function_name`.
+ """
+ python_breakpoint = os.getenv('PYTHONBREAKPOINT')
+ # The special value '0' for `$PYTHONBREAKPOINT` means "do not use a debugger".
+ # We don't respect it (if the user explicitly asks to debug) but shouldn't try
+ # to import a module with this name.
+ if python_breakpoint and python_breakpoint != '0':
+ debugger_module_import = python_breakpoint.rsplit('.', 1)[0]
+ try:
+ debugger_module = importlib.import_module(debugger_module_import)
+ except ImportError:
+ logging.warning(
+ (
+ 'Could not import $PYTHONBREAKPOINT debugger module %r, '
+ 'falling back to PDB'
+ ),
+ debugger_module_import,
+ )
+ else:
+ if hasattr(debugger_module, function_name):
+ return debugger_module
+ logging.warning(
+ '$PYTHONBREAKPOINT debugger %r has no function %r, '
+ 'falling back to PDB',
+ debugger_module_import,
+ function_name,
+ )
+ return pdb
+
+
+# If main() exits via an abnormal exception, call into these
+# handlers before exiting.
+EXCEPTION_HANDLERS = []
+
+
+class Error(Exception):
+ pass
+
+
+class UsageError(Error):
+ """Exception raised when the arguments supplied by the user are invalid.
+
+ Raise this when the arguments supplied are invalid from the point of
+ view of the application. For example when two mutually exclusive
+ flags have been supplied or when there are not enough non-flag
+ arguments. It is distinct from flags.Error which covers the lower
+ level of parsing and validating individual flags.
+ """
+
+ def __init__(self, message, exitcode=1):
+ super().__init__(message)
+ self.exitcode = exitcode
+
+
+class HelpFlag(flags.BooleanFlag):
+ """Special boolean flag that displays usage and raises SystemExit."""
+ NAME = 'help'
+ SHORT_NAME = '?'
+
+ def __init__(self):
+ super().__init__(
+ self.NAME,
+ False,
+ 'show this help',
+ short_name=self.SHORT_NAME,
+ allow_hide_cpp=True,
+ )
+
+ def parse(self, arg):
+ if self._parse(arg):
+ usage(shorthelp=True, writeto_stdout=True)
+ # Advertise --helpfull on stdout, since usage() was on stdout.
+ print()
+ print('Try --helpfull to get a list of all flags.')
+ sys.exit(1)
+
+
+class HelpshortFlag(HelpFlag):
+ """--helpshort is an alias for --help."""
+ NAME = 'helpshort'
+ SHORT_NAME = None
+
+
+class HelpfullFlag(flags.BooleanFlag):
+ """Display help for flags in the main module and all dependent modules."""
+
+ def __init__(self):
+ super().__init__('helpfull', False, 'show full help', allow_hide_cpp=True)
+
+ def parse(self, arg):
+ if self._parse(arg):
+ usage(writeto_stdout=True)
+ sys.exit(1)
+
+
+class HelpXMLFlag(flags.BooleanFlag):
+ """Similar to HelpfullFlag, but generates output in XML format."""
+
+ def __init__(self):
+ super().__init__(
+ 'helpxml',
+ False,
+ 'like --helpfull, but generates XML output',
+ allow_hide_cpp=True,
+ )
+
+ def parse(self, arg):
+ if self._parse(arg):
+ flags.FLAGS.write_help_in_xml_format(sys.stdout)
+ sys.exit(1)
+
+
+def parse_flags_with_usage(args):
+ """Tries to parse the flags, print usage, and exit if unparsable.
+
+ Args:
+ args: [str], a non-empty list of the command line arguments including
+ program name.
+
+ Returns:
+ [str], a non-empty list of remaining command line arguments after parsing
+ flags, including program name.
+ """
+ try:
+ return FLAGS(args)
+ except flags.Error as error:
+ message = str(error)
+ if '\n' in message:
+ final_message = 'FATAL Flags parsing error:\n%s\n' % textwrap.indent(
+ message, ' ')
+ else:
+ final_message = 'FATAL Flags parsing error: %s\n' % message
+ sys.stderr.write(final_message)
+ sys.stderr.write('Pass --helpshort or --helpfull to see help on flags.\n')
+ sys.exit(1)
+
+
+_define_help_flags_called = False
+
+
+def define_help_flags():
+ """Registers help flags. Idempotent."""
+ # Use a global to ensure idempotence.
+ global _define_help_flags_called
+
+ if not _define_help_flags_called:
+ flags.DEFINE_flag(HelpFlag())
+ flags.DEFINE_flag(HelpshortFlag()) # alias for --help
+ flags.DEFINE_flag(HelpfullFlag())
+ flags.DEFINE_flag(HelpXMLFlag())
+ _define_help_flags_called = True
+
+
+def _register_and_parse_flags_with_usage(
+ argv=None,
+ flags_parser=parse_flags_with_usage,
+):
+ """Registers help flags, parses arguments and shows usage if appropriate.
+
+ This also calls sys.exit(0) if flag --only_check_args is True.
+
+ Args:
+ argv: [str], a non-empty list of the command line arguments including
+ program name, sys.argv is used if None.
+ flags_parser: Callable[[List[str]], Any], the function used to parse flags.
+ The return value of this function is passed to `main` untouched. It must
+ guarantee FLAGS is parsed after this function is called.
+
+ Returns:
+ The return value of `flags_parser`. When using the default `flags_parser`,
+ it returns the following:
+ [str], a non-empty list of remaining command line arguments after parsing
+ flags, including program name.
+
+ Raises:
+ Error: Raised when flags_parser is called, but FLAGS is not parsed.
+ SystemError: Raised when it's called more than once.
+ """
+ # fmt: on
+ if _register_and_parse_flags_with_usage.done:
+ raise SystemError('Flag registration can be done only once.')
+
+ define_help_flags()
+
+ original_argv = sys.argv if argv is None else argv
+ args_to_main = flags_parser(original_argv)
+ if not FLAGS.is_parsed():
+ raise Error('FLAGS must be parsed after flags_parser is called.')
+
+ # Exit when told so.
+ if FLAGS.only_check_args:
+ sys.exit(0)
+ # Immediately after flags are parsed, bump verbosity to INFO if the flag has
+ # not been set.
+ if FLAGS['verbosity'].using_default_value:
+ FLAGS.verbosity = 0
+ _register_and_parse_flags_with_usage.done = True
+
+ return args_to_main
+
+_register_and_parse_flags_with_usage.done = False
+
+
+def _run_main(main, argv):
+ """Calls main, optionally with a debugger or profiler."""
+ if FLAGS.run_with_pdb:
+ sys.exit(_get_debugger_module_with_function('runcall').runcall(main, argv))
+ elif FLAGS.run_with_profiling or FLAGS.profile_file:
+ # Avoid import overhead since most apps (including performance-sensitive
+ # ones) won't be run with profiling.
+ # pylint: disable=g-import-not-at-top
+ import atexit
+ if FLAGS.use_cprofile_for_profiling:
+ import cProfile as profile
+ else:
+ import profile
+ profiler = profile.Profile()
+ if FLAGS.profile_file:
+ atexit.register(profiler.dump_stats, FLAGS.profile_file)
+ else:
+ atexit.register(profiler.print_stats)
+ sys.exit(profiler.runcall(main, argv))
+ else:
+ sys.exit(main(argv))
+
+
+def _call_exception_handlers(exception):
+ """Calls any installed exception handlers."""
+ for handler in EXCEPTION_HANDLERS:
+ try:
+ if handler.wants(exception):
+ handler.handle(exception)
+ except: # pylint: disable=bare-except
+ try:
+ # We don't want to stop for exceptions in the exception handlers but
+ # we shouldn't hide them either.
+ logging.error(traceback.format_exc())
+ except: # pylint: disable=bare-except
+ # In case even the logging statement fails, ignore.
+ pass
+
+
+def run(
+ main,
+ argv=None,
+ flags_parser=parse_flags_with_usage,
+):
+ """Begins executing the program.
+
+ Args:
+ main: The main function to execute. It takes an single argument "argv",
+ which is a list of command line arguments with parsed flags removed.
+ The return value is passed to `sys.exit`, and so for example
+ a return value of 0 or None results in a successful termination, whereas
+ a return value of 1 results in abnormal termination.
+ For more details, see https://docs.python.org/3/library/sys#sys.exit
+ argv: A non-empty list of the command line arguments including program name,
+ sys.argv is used if None.
+ flags_parser: Callable[[List[str]], Any], the function used to parse flags.
+ The return value of this function is passed to `main` untouched.
+ It must guarantee FLAGS is parsed after this function is called.
+ Should be passed as a keyword-only arg which will become mandatory in a
+ future release.
+ - Parses command line flags with the flag module.
+ - If there are any errors, prints usage().
+ - Calls main() with the remaining arguments.
+ - If main() raises a UsageError, prints usage and the error message.
+ """
+ # fmt: on
+ try:
+ args = _run_init(
+ sys.argv if argv is None else argv,
+ flags_parser,
+ )
+ while _init_callbacks:
+ callback = _init_callbacks.popleft()
+ callback()
+ try:
+ _run_main(main, args)
+ except UsageError as error:
+ usage(shorthelp=True, detailed_error=error, exitcode=error.exitcode)
+ except:
+ exc = sys.exc_info()[1]
+ # Don't try to post-mortem debug successful SystemExits, since those
+ # mean there wasn't actually an error. In particular, the test framework
+ # raises SystemExit(False) even if all tests passed.
+ if isinstance(exc, SystemExit) and not exc.code:
+ raise
+
+ # Check the tty so that we don't hang waiting for input in an
+ # non-interactive scenario.
+ if FLAGS.pdb_post_mortem and sys.stdout.isatty():
+ traceback.print_exc()
+ print()
+ print(' *** Entering post-mortem debugging ***')
+ print()
+ _get_debugger_module_with_function('post_mortem').post_mortem()
+ raise
+ except Exception as e:
+ _call_exception_handlers(e)
+ raise
+
+# Callbacks which have been deferred until after _run_init has been called.
+_init_callbacks = collections.deque()
+
+
+def call_after_init(callback):
+ """Calls the given callback only once ABSL has finished initialization.
+
+ If ABSL has already finished initialization when ``call_after_init`` is
+ called then the callback is executed immediately, otherwise `callback` is
+ stored to be executed after ``app.run`` has finished initializing (aka. just
+ before the main function is called).
+
+ If called after ``app.run``, this is equivalent to calling ``callback()`` in
+ the caller thread. If called before ``app.run``, callbacks are run
+ sequentially (in an undefined order) in the same thread as ``app.run``.
+
+ Args:
+ callback: a callable to be called once ABSL has finished initialization.
+ This may be immediate if initialization has already finished. It
+ takes no arguments and returns nothing.
+ """
+ if _run_init.done:
+ callback()
+ else:
+ _init_callbacks.append(callback)
+
+
+def _run_init(
+ argv,
+ flags_parser,
+):
+ """Does one-time initialization and re-parses flags on rerun."""
+ if _run_init.done:
+ return flags_parser(argv)
+ command_name.make_process_name_useful()
+ # Set up absl logging handler.
+ logging.use_absl_handler()
+ args = _register_and_parse_flags_with_usage(
+ argv=argv,
+ flags_parser=flags_parser,
+ )
+ if faulthandler:
+ try:
+ faulthandler.enable()
+ except Exception: # pylint: disable=broad-except
+ # Some tests verify stderr output very closely, so don't print anything.
+ # Disabled faulthandler is a low-impact error.
+ pass
+ _run_init.done = True
+ return args
+
+
+_run_init.done = False
+
+
+def usage(shorthelp=False, writeto_stdout=False, detailed_error=None,
+ exitcode=None):
+ """Writes __main__'s docstring to stderr with some help text.
+
+ Args:
+ shorthelp: bool, if True, prints only flags from the main module,
+ rather than all flags.
+ writeto_stdout: bool, if True, writes help message to stdout,
+ rather than to stderr.
+ detailed_error: str, additional detail about why usage info was presented.
+ exitcode: optional integer, if set, exits with this status code after
+ writing help.
+ """
+ if writeto_stdout:
+ stdfile = sys.stdout
+ else:
+ stdfile = sys.stderr
+
+ doc = sys.modules['__main__'].__doc__
+ if not doc:
+ doc = '\nUSAGE: %s [flags]\n' % sys.argv[0]
+ doc = flags.text_wrap(doc, indent=' ', firstline_indent='')
+ else:
+ # Replace all '%s' with sys.argv[0], and all '%%' with '%'.
+ num_specifiers = doc.count('%') - 2 * doc.count('%%')
+ try:
+ doc %= (sys.argv[0],) * num_specifiers
+ except (OverflowError, TypeError, ValueError):
+ # Just display the docstring as-is.
+ pass
+ if shorthelp:
+ flag_str = FLAGS.main_module_help()
+ else:
+ flag_str = FLAGS.get_help()
+ try:
+ stdfile.write(doc)
+ if flag_str:
+ stdfile.write('\nflags:\n')
+ stdfile.write(flag_str)
+ stdfile.write('\n')
+ if detailed_error is not None:
+ stdfile.write('\n%s\n' % detailed_error)
+ except OSError as e:
+ # We avoid printing a huge backtrace if we get EPIPE, because
+ # "foo.par --help | less" is a frequent use case.
+ if e.errno != errno.EPIPE:
+ raise
+ if exitcode is not None:
+ sys.exit(exitcode)
+
+
+class ExceptionHandler:
+ """Base exception handler from which other may inherit."""
+
+ def wants(self, exc):
+ """Returns whether this handler wants to handle the exception or not.
+
+ This base class returns True for all exceptions by default. Override in
+ subclass if it wants to be more selective.
+
+ Args:
+ exc: Exception, the current exception.
+ """
+ del exc # Unused.
+ return True
+
+ def handle(self, exc):
+ """Do something with the current exception.
+
+ Args:
+ exc: Exception, the current exception
+
+ This method must be overridden.
+ """
+ raise NotImplementedError()
+
+
+def install_exception_handler(handler):
+ """Installs an exception handler.
+
+ Args:
+ handler: ExceptionHandler, the exception handler to install.
+
+ Raises:
+ TypeError: Raised when the handler was not of the correct type.
+
+ All installed exception handlers will be called if main() exits via
+ an abnormal exception, i.e. not one of SystemExit, KeyboardInterrupt,
+ FlagsError or UsageError.
+ """
+ if not isinstance(handler, ExceptionHandler):
+ raise TypeError('handler of type %s does not inherit from ExceptionHandler'
+ % type(handler))
+ EXCEPTION_HANDLERS.append(handler)
diff --git a/venv/Lib/site-packages/absl/app.pyi b/venv/Lib/site-packages/absl/app.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..882ef2b9856231e09e65d0213abdd775a8efe13e
--- /dev/null
+++ b/venv/Lib/site-packages/absl/app.pyi
@@ -0,0 +1,89 @@
+from collections.abc import Callable
+from typing import Any, NoReturn, TypeVar, overload
+
+from absl.flags import _flag
+
+_MainArgs = TypeVar('_MainArgs')
+_Exc = TypeVar('_Exc', bound=Exception)
+
+class ExceptionHandler:
+
+ def wants(self, exc: _Exc) -> bool:
+ ...
+
+ def handle(self, exc: _Exc):
+ ...
+
+EXCEPTION_HANDLERS: list[ExceptionHandler] = ...
+
+class HelpFlag(_flag.BooleanFlag):
+ def __init__(self):
+ ...
+
+class HelpshortFlag(HelpFlag):
+ ...
+
+class HelpfullFlag(_flag.BooleanFlag):
+ def __init__(self):
+ ...
+
+class HelpXMLFlag(_flag.BooleanFlag):
+ def __init__(self):
+ ...
+
+def define_help_flags() -> None:
+ ...
+
+@overload
+def usage(shorthelp: bool | int = ...,
+ writeto_stdout: bool | int = ...,
+ detailed_error: Any | None = ...,
+ exitcode: None = ...) -> None:
+ ...
+
+@overload
+def usage(shorthelp: bool | int,
+ writeto_stdout: bool | int,
+ detailed_error: Any | None,
+ exitcode: int) -> NoReturn:
+ ...
+
+@overload
+def usage(shorthelp: bool | int = ...,
+ writeto_stdout: bool | int = ...,
+ detailed_error: Any | None = ...,
+ *,
+ exitcode: int) -> NoReturn:
+ ...
+
+def install_exception_handler(handler: ExceptionHandler) -> None:
+ ...
+
+class Error(Exception):
+ ...
+
+class UsageError(Error):
+ exitcode: int
+
+def parse_flags_with_usage(args: list[str]) -> list[str]:
+ ...
+
+def call_after_init(callback: Callable[[], Any]) -> None:
+ ...
+
+# Without the flag_parser argument, `main` should require a List[str].
+@overload
+def run(
+ main: Callable[[list[str]], Any],
+ argv: list[str] | None = ...,
+) -> NoReturn:
+ ...
+
+@overload
+def run(
+ main: Callable[[_MainArgs], Any],
+ argv: list[str] | None = ...,
+ *,
+ flags_parser: Callable[[list[str]], _MainArgs],
+) -> NoReturn:
+ ...
diff --git a/venv/Lib/site-packages/absl/command_name.py b/venv/Lib/site-packages/absl/command_name.py
new file mode 100644
index 0000000000000000000000000000000000000000..86f81af8ed9fbc98dd46a3171631163ae8f693a5
--- /dev/null
+++ b/venv/Lib/site-packages/absl/command_name.py
@@ -0,0 +1,63 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A tiny stand alone library to change the kernel process name on Linux."""
+
+import os
+import sys
+
+# This library must be kept small and stand alone. It is used by small things
+# that require no extension modules.
+
+
+def make_process_name_useful():
+ """Sets the process name to something better than 'python' if possible."""
+ set_kernel_process_name(os.path.basename(sys.argv[0]))
+
+
+def set_kernel_process_name(name):
+ """Changes the Kernel's /proc/self/status process name on Linux.
+
+ The kernel name is NOT what will be shown by the ps or top command.
+ It is a 15 character string stored in the kernel's process table that
+ is included in the kernel log when a process is OOM killed.
+ The first 15 bytes of name are used. Non-ASCII unicode is replaced with '?'.
+
+ Does nothing if /proc/self/comm cannot be written or prctl() fails.
+
+ Args:
+ name: bytes|unicode, the Linux kernel's command name to set.
+ """
+ if not isinstance(name, bytes):
+ name = name.encode('ascii', 'replace')
+ try:
+ # This is preferred to using ctypes to try and call prctl() when possible.
+ with open('/proc/self/comm', 'wb') as proc_comm:
+ proc_comm.write(name[:15])
+ except OSError:
+ try:
+ import ctypes # pylint: disable=g-import-not-at-top
+ except ImportError:
+ return # No ctypes.
+ try:
+ libc = ctypes.CDLL('libc.so.6')
+ except OSError:
+ return # No libc.so.6.
+ pr_set_name = ctypes.c_ulong(15) # linux/prctl.h PR_SET_NAME value.
+ zero = ctypes.c_ulong(0)
+ try:
+ libc.prctl(pr_set_name, name, zero, zero, zero)
+ # Ignore the prctl return value. Nothing we can do if it errored.
+ except AttributeError:
+ return # No prctl.
diff --git a/venv/Lib/site-packages/absl/flags/__init__.py b/venv/Lib/site-packages/absl/flags/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..06401900d2d4c6da61f6c532e330eb6a3fcc39e7
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/__init__.py
@@ -0,0 +1,220 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""This package is used to define and parse command line flags.
+
+This package defines a *distributed* flag-definition policy: rather than
+an application having to define all flags in or near main(), each Python
+module defines flags that are useful to it. When one Python module
+imports another, it gains access to the other's flags. (This is
+implemented by having all modules share a common, global registry object
+containing all the flag information.)
+
+Flags are defined through the use of one of the DEFINE_xxx functions.
+The specific function used determines how the flag is parsed, checked,
+and optionally type-converted, when it's seen on the command line.
+"""
+
+import sys
+
+from absl.flags import _argument_parser
+from absl.flags import _defines
+from absl.flags import _exceptions
+from absl.flags import _flag
+from absl.flags import _flagvalues
+from absl.flags import _helpers
+from absl.flags import _validators
+
+__all__ = (
+ 'DEFINE',
+ 'DEFINE_flag',
+ 'DEFINE_string',
+ 'DEFINE_boolean',
+ 'DEFINE_bool',
+ 'DEFINE_float',
+ 'DEFINE_integer',
+ 'DEFINE_enum',
+ 'DEFINE_enum_class',
+ 'DEFINE_list',
+ 'DEFINE_spaceseplist',
+ 'DEFINE_multi',
+ 'DEFINE_multi_string',
+ 'DEFINE_multi_integer',
+ 'DEFINE_multi_float',
+ 'DEFINE_multi_enum',
+ 'DEFINE_multi_enum_class',
+ 'DEFINE_alias',
+ # Flag validators.
+ 'register_validator',
+ 'validator',
+ 'register_multi_flags_validator',
+ 'multi_flags_validator',
+ 'mark_flag_as_required',
+ 'mark_flags_as_required',
+ 'mark_flags_as_mutual_exclusive',
+ 'mark_bool_flags_as_mutual_exclusive',
+ # Flag modifiers.
+ 'set_default',
+ 'override_value',
+ # Key flag related functions.
+ 'declare_key_flag',
+ 'adopt_module_key_flags',
+ 'disclaim_key_flags',
+ # Module exceptions.
+ 'Error',
+ 'CantOpenFlagFileError',
+ 'DuplicateFlagError',
+ 'IllegalFlagValueError',
+ 'UnrecognizedFlagError',
+ 'UnparsedFlagAccessError',
+ 'ValidationError',
+ 'FlagNameConflictsWithMethodError',
+ # Public classes.
+ 'Flag',
+ 'BooleanFlag',
+ 'EnumFlag',
+ 'EnumClassFlag',
+ 'MultiFlag',
+ 'MultiEnumClassFlag',
+ 'FlagHolder',
+ 'FlagValues',
+ 'ArgumentParser',
+ 'BooleanParser',
+ 'EnumParser',
+ 'EnumClassParser',
+ 'ArgumentSerializer',
+ 'FloatParser',
+ 'IntegerParser',
+ 'BaseListParser',
+ 'ListParser',
+ 'ListSerializer',
+ 'EnumClassListSerializer',
+ 'CsvListSerializer',
+ 'WhitespaceSeparatedListParser',
+ 'EnumClassSerializer',
+ # Helper functions.
+ 'get_help_width',
+ 'text_wrap',
+ 'flag_dict_to_args',
+ 'doc_to_help',
+ # The global FlagValues instance.
+ 'FLAGS',
+)
+
+# Initialize the FLAGS_MODULE as early as possible.
+# It's only used by adopt_module_key_flags to take SPECIAL_FLAGS into account.
+_helpers.FLAGS_MODULE = sys.modules[__name__]
+
+# Add current module to disclaimed module ids.
+_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
+
+# DEFINE functions. They are explained in more details in the module doc string.
+# pylint: disable=invalid-name
+DEFINE = _defines.DEFINE
+DEFINE_flag = _defines.DEFINE_flag
+DEFINE_string = _defines.DEFINE_string
+DEFINE_boolean = _defines.DEFINE_boolean
+DEFINE_bool = DEFINE_boolean # Match C++ API.
+DEFINE_float = _defines.DEFINE_float
+DEFINE_integer = _defines.DEFINE_integer
+DEFINE_enum = _defines.DEFINE_enum
+DEFINE_enum_class = _defines.DEFINE_enum_class
+DEFINE_list = _defines.DEFINE_list
+DEFINE_spaceseplist = _defines.DEFINE_spaceseplist
+DEFINE_multi = _defines.DEFINE_multi
+DEFINE_multi_string = _defines.DEFINE_multi_string
+DEFINE_multi_integer = _defines.DEFINE_multi_integer
+DEFINE_multi_float = _defines.DEFINE_multi_float
+DEFINE_multi_enum = _defines.DEFINE_multi_enum
+DEFINE_multi_enum_class = _defines.DEFINE_multi_enum_class
+DEFINE_alias = _defines.DEFINE_alias
+# pylint: enable=invalid-name
+
+# Flag validators.
+register_validator = _validators.register_validator
+validator = _validators.validator
+register_multi_flags_validator = _validators.register_multi_flags_validator
+multi_flags_validator = _validators.multi_flags_validator
+mark_flag_as_required = _validators.mark_flag_as_required
+mark_flags_as_required = _validators.mark_flags_as_required
+mark_flags_as_mutual_exclusive = _validators.mark_flags_as_mutual_exclusive
+mark_bool_flags_as_mutual_exclusive = _validators.mark_bool_flags_as_mutual_exclusive
+
+# Flag modifiers.
+set_default = _defines.set_default
+override_value = _defines.override_value
+
+# Key flag related functions.
+declare_key_flag = _defines.declare_key_flag
+adopt_module_key_flags = _defines.adopt_module_key_flags
+disclaim_key_flags = _defines.disclaim_key_flags
+
+# Module exceptions.
+# pylint: disable=invalid-name
+Error = _exceptions.Error
+CantOpenFlagFileError = _exceptions.CantOpenFlagFileError
+DuplicateFlagError = _exceptions.DuplicateFlagError
+IllegalFlagValueError = _exceptions.IllegalFlagValueError
+UnrecognizedFlagError = _exceptions.UnrecognizedFlagError
+UnparsedFlagAccessError = _exceptions.UnparsedFlagAccessError
+ValidationError = _exceptions.ValidationError
+FlagNameConflictsWithMethodError = _exceptions.FlagNameConflictsWithMethodError
+
+# Public classes.
+Flag = _flag.Flag
+BooleanFlag = _flag.BooleanFlag
+EnumFlag = _flag.EnumFlag
+EnumClassFlag = _flag.EnumClassFlag
+MultiFlag = _flag.MultiFlag
+MultiEnumClassFlag = _flag.MultiEnumClassFlag
+FlagHolder = _flagvalues.FlagHolder
+FlagValues = _flagvalues.FlagValues
+ArgumentParser = _argument_parser.ArgumentParser
+BooleanParser = _argument_parser.BooleanParser
+EnumParser = _argument_parser.EnumParser
+EnumClassParser = _argument_parser.EnumClassParser
+ArgumentSerializer = _argument_parser.ArgumentSerializer
+FloatParser = _argument_parser.FloatParser
+IntegerParser = _argument_parser.IntegerParser
+BaseListParser = _argument_parser.BaseListParser
+ListParser = _argument_parser.ListParser
+ListSerializer = _argument_parser.ListSerializer
+EnumClassListSerializer = _argument_parser.EnumClassListSerializer
+CsvListSerializer = _argument_parser.CsvListSerializer
+WhitespaceSeparatedListParser = _argument_parser.WhitespaceSeparatedListParser
+EnumClassSerializer = _argument_parser.EnumClassSerializer
+# pylint: enable=invalid-name
+
+# Helper functions.
+get_help_width = _helpers.get_help_width
+text_wrap = _helpers.text_wrap
+flag_dict_to_args = _helpers.flag_dict_to_args
+doc_to_help = _helpers.doc_to_help
+
+# Special flags.
+_helpers.SPECIAL_FLAGS = FlagValues()
+
+DEFINE_string(
+ 'flagfile', '',
+ 'Insert flag definitions from the given file into the command line.',
+ _helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types
+
+DEFINE_string('undefok', '',
+ 'comma-separated list of flag names that it is okay to specify '
+ 'on the command line even if the program does not define a flag '
+ 'with that name. IMPORTANT: flags in this list that have '
+ 'arguments MUST use the --flag=value format.',
+ _helpers.SPECIAL_FLAGS) # pytype: disable=wrong-arg-types
+
+#: The global FlagValues instance.
+FLAGS = _flagvalues.FLAGS
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..8eeca49fe99f8560df42acb41696434fbb2a37b0
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_argument_parser.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_argument_parser.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ef94b8251754b96e2e717bb52170f058a80c913e
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_argument_parser.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_defines.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_defines.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..4d37bb9f35f540605591f1ddedccfc3e0deada1a
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_defines.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_exceptions.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_exceptions.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..77d48abb5dd5d74bbf4ead365dd57d81a4dd8c3c
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_exceptions.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_flag.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_flag.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..06499713c775480f3b9af053afa17f3fe817e39d
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_flag.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_flagvalues.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_flagvalues.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..ecd7097353b79f5bd56fa2a38f9aed2dc4ef8393
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_flagvalues.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_helpers.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_helpers.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..c95c644b8abba52752089e8127f85a76bb5c73f6
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_helpers.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_validators.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_validators.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..59fb86c2592d32b5d9242ca8a0fbb45ce29a7d89
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_validators.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/_validators_classes.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/_validators_classes.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..0817a2d89cad9356940f84d21eb3ce4479f0ea28
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/_validators_classes.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/__pycache__/argparse_flags.cpython-311.pyc b/venv/Lib/site-packages/absl/flags/__pycache__/argparse_flags.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..206e438810dff9660fb0334e83ef0fce2ca9d84d
Binary files /dev/null and b/venv/Lib/site-packages/absl/flags/__pycache__/argparse_flags.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/flags/_argument_parser.py b/venv/Lib/site-packages/absl/flags/_argument_parser.py
new file mode 100644
index 0000000000000000000000000000000000000000..9aa7a0fb6f2f6b37e7a9f82cfc906427ed6e89e7
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_argument_parser.py
@@ -0,0 +1,632 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contains base classes used to parse and convert arguments.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+import collections
+from collections.abc import Iterable, Sequence
+import csv
+import enum
+import io
+import string
+from typing import Any, Generic, TypeVar
+from xml.dom import minidom
+
+from absl.flags import _helpers
+
+_T = TypeVar('_T')
+_ET = TypeVar('_ET', bound=enum.Enum)
+_N = TypeVar('_N', int, float)
+
+
+class _ArgumentParserCache(type):
+ """Metaclass used to cache and share argument parsers among flags."""
+
+ _instances: dict[Any, Any] = {}
+
+ def __call__(cls, *args, **kwargs):
+ """Returns an instance of the argument parser cls.
+
+ This method overrides behavior of the __new__ methods in
+ all subclasses of ArgumentParser (inclusive). If an instance
+ for cls with the same set of arguments exists, this instance is
+ returned, otherwise a new instance is created.
+
+ If any keyword arguments are defined, or the values in args
+ are not hashable, this method always returns a new instance of
+ cls.
+
+ Args:
+ *args: Positional initializer arguments.
+ **kwargs: Initializer keyword arguments.
+
+ Returns:
+ An instance of cls, shared or new.
+ """
+ if kwargs:
+ return type.__call__(cls, *args, **kwargs)
+ else:
+ instances = cls._instances
+ key = (cls,) + tuple(args)
+ try:
+ return instances[key]
+ except KeyError:
+ # No cache entry for key exists, create a new one.
+ return instances.setdefault(key, type.__call__(cls, *args))
+ except TypeError:
+ # An object in args cannot be hashed, always return
+ # a new instance.
+ return type.__call__(cls, *args)
+
+
+class ArgumentParser(Generic[_T], metaclass=_ArgumentParserCache):
+ """Base class used to parse and convert arguments.
+
+ The :meth:`parse` method checks to make sure that the string argument is a
+ legal value and convert it to a native type. If the value cannot be
+ converted, it should throw a ``ValueError`` exception with a human
+ readable explanation of why the value is illegal.
+
+ Subclasses should also define a syntactic_help string which may be
+ presented to the user to describe the form of the legal values.
+
+ Argument parser classes must be stateless, since instances are cached
+ and shared between flags. Initializer arguments are allowed, but all
+ member variables must be derived from initializer arguments only.
+ """
+
+ syntactic_help: str = ''
+
+ def parse(self, argument: str) -> _T | None:
+ """Parses the string argument and returns the native value.
+
+ By default it returns its argument unmodified.
+
+ Args:
+ argument: string argument passed in the commandline.
+
+ Raises:
+ ValueError: Raised when it fails to parse the argument.
+ TypeError: Raised when the argument has the wrong type.
+
+ Returns:
+ The parsed value in native type.
+ """
+ if not isinstance(argument, str):
+ raise TypeError('flag value must be a string, found "{}"'.format(
+ type(argument)))
+ return argument # type: ignore[return-value]
+
+ def flag_type(self) -> str:
+ """Returns a string representing the type of the flag."""
+ return 'string'
+
+ def _custom_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ """Returns a list of minidom.Element to add additional flag information.
+
+ Args:
+ doc: minidom.Document, the DOM document it should create nodes from.
+ """
+ del doc # Unused.
+ return []
+
+
+class ArgumentSerializer(Generic[_T]):
+ """Base class for generating string representations of a flag value."""
+
+ def serialize(self, value: _T) -> str:
+ """Returns a serialized string of the value."""
+ return str(value)
+
+
+class NumericParser(ArgumentParser[_N]):
+ """Parser of numeric values.
+
+ Parsed value may be bounded to a given upper and lower bound.
+ """
+
+ lower_bound: _N | None
+ upper_bound: _N | None
+
+ def is_outside_bounds(self, val: _N) -> bool:
+ """Returns whether the value is outside the bounds or not."""
+ return ((self.lower_bound is not None and val < self.lower_bound) or
+ (self.upper_bound is not None and val > self.upper_bound))
+
+ def parse(self, argument: str | _N) -> _N:
+ """See base class."""
+ val = self.convert(argument)
+ if self.is_outside_bounds(val):
+ raise ValueError('%s is not %s' % (val, self.syntactic_help))
+ return val
+
+ def _custom_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = []
+ if self.lower_bound is not None:
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'lower_bound', self.lower_bound))
+ if self.upper_bound is not None:
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'upper_bound', self.upper_bound))
+ return elements
+
+ def convert(self, argument: str | _N) -> _N:
+ """Returns the correct numeric value of argument.
+
+ Subclass must implement this method, and raise TypeError if argument is not
+ string or has the right numeric type.
+
+ Args:
+ argument: string argument passed in the commandline, or the numeric type.
+
+ Raises:
+ TypeError: Raised when argument is not a string or the right numeric type.
+ ValueError: Raised when failed to convert argument to the numeric value.
+ """
+ raise NotImplementedError
+
+
+class FloatParser(NumericParser[float]):
+ """Parser of floating point values.
+
+ Parsed value may be bounded to a given upper and lower bound.
+ """
+ number_article = 'a'
+ number_name = 'number'
+ syntactic_help = ' '.join((number_article, number_name))
+
+ def __init__(
+ self,
+ lower_bound: float | None = None,
+ upper_bound: float | None = None,
+ ) -> None:
+ super().__init__()
+ self.lower_bound = lower_bound
+ self.upper_bound = upper_bound
+ sh = self.syntactic_help
+ if lower_bound is not None and upper_bound is not None:
+ sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound))
+ elif lower_bound == 0:
+ sh = 'a non-negative %s' % self.number_name
+ elif upper_bound == 0:
+ sh = 'a non-positive %s' % self.number_name
+ elif upper_bound is not None:
+ sh = '%s <= %s' % (self.number_name, upper_bound)
+ elif lower_bound is not None:
+ sh = '%s >= %s' % (self.number_name, lower_bound)
+ self.syntactic_help = sh
+
+ def convert(self, argument: int | float | str) -> float:
+ """Returns the float value of argument."""
+ if (
+ (isinstance(argument, int) and not isinstance(argument, bool))
+ or isinstance(argument, float)
+ or isinstance(argument, str)
+ ):
+ return float(argument)
+ else:
+ raise TypeError(
+ 'Expect argument to be a string, int, or float, found {}'.format(
+ type(argument)))
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return 'float'
+
+
+class IntegerParser(NumericParser[int]):
+ """Parser of an integer value.
+
+ Parsed value may be bounded to a given upper and lower bound.
+ """
+ number_article = 'an'
+ number_name = 'integer'
+ syntactic_help = ' '.join((number_article, number_name))
+
+ def __init__(
+ self, lower_bound: int | None = None, upper_bound: int | None = None
+ ) -> None:
+ super().__init__()
+ self.lower_bound = lower_bound
+ self.upper_bound = upper_bound
+ sh = self.syntactic_help
+ if lower_bound is not None and upper_bound is not None:
+ sh = ('%s in the range [%s, %s]' % (sh, lower_bound, upper_bound))
+ elif lower_bound == 1:
+ sh = 'a positive %s' % self.number_name
+ elif upper_bound == -1:
+ sh = 'a negative %s' % self.number_name
+ elif lower_bound == 0:
+ sh = 'a non-negative %s' % self.number_name
+ elif upper_bound == 0:
+ sh = 'a non-positive %s' % self.number_name
+ elif upper_bound is not None:
+ sh = '%s <= %s' % (self.number_name, upper_bound)
+ elif lower_bound is not None:
+ sh = '%s >= %s' % (self.number_name, lower_bound)
+ self.syntactic_help = sh
+
+ def convert(self, argument: int | str) -> int:
+ """Returns the int value of argument."""
+ if isinstance(argument, int) and not isinstance(argument, bool):
+ return argument
+ elif isinstance(argument, str):
+ base = 10
+ if len(argument) > 2 and argument[0] == '0':
+ if argument[1] == 'o':
+ base = 8
+ elif argument[1] == 'x':
+ base = 16
+ return int(argument, base)
+ else:
+ raise TypeError('Expect argument to be a string or int, found {}'.format(
+ type(argument)))
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return 'int'
+
+
+class BooleanParser(ArgumentParser[bool]):
+ """Parser of boolean values."""
+
+ def parse(self, argument: str | int) -> bool:
+ """See base class."""
+ if isinstance(argument, str):
+ if argument.lower() in ('true', 't', '1'):
+ return True
+ elif argument.lower() in ('false', 'f', '0'):
+ return False
+ else:
+ raise ValueError('Non-boolean argument to boolean flag', argument)
+ elif isinstance(argument, int):
+ # Only allow bool or integer 0, 1.
+ # Note that float 1.0 == True, 0.0 == False.
+ bool_value = bool(argument)
+ if argument == bool_value:
+ return bool_value
+ else:
+ raise ValueError('Non-boolean argument to boolean flag', argument)
+
+ raise TypeError('Non-boolean argument to boolean flag', argument)
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return 'bool'
+
+
+class EnumParser(ArgumentParser[str]):
+ """Parser of a string enum value (a string value from a given set)."""
+
+ def __init__(
+ self, enum_values: Iterable[str], case_sensitive: bool = True
+ ) -> None:
+ """Initializes EnumParser.
+
+ Args:
+ enum_values: [str], a non-empty list of string values in the enum.
+ case_sensitive: bool, whether or not the enum is to be case-sensitive.
+
+ Raises:
+ ValueError: When enum_values is empty.
+ """
+ if not enum_values:
+ raise ValueError(f'enum_values cannot be empty, found "{enum_values}"')
+ if isinstance(enum_values, str):
+ raise ValueError(f'enum_values cannot be a str, found "{enum_values}"')
+ super().__init__()
+ self.enum_values = list(enum_values)
+ self.case_sensitive = case_sensitive
+
+ def parse(self, argument: str) -> str:
+ """Determines validity of argument and returns the correct element of enum.
+
+ Args:
+ argument: str, the supplied flag value.
+
+ Returns:
+ The first matching element from enum_values.
+
+ Raises:
+ ValueError: Raised when argument didn't match anything in enum.
+ """
+ if self.case_sensitive:
+ if argument not in self.enum_values:
+ raise ValueError('value should be one of <%s>' %
+ '|'.join(self.enum_values))
+ else:
+ return argument
+ else:
+ if argument.upper() not in [value.upper() for value in self.enum_values]:
+ raise ValueError('value should be one of <%s>' %
+ '|'.join(self.enum_values))
+ else:
+ return [value for value in self.enum_values
+ if value.upper() == argument.upper()][0]
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return 'string enum'
+
+
+class EnumClassParser(ArgumentParser[_ET]):
+ """Parser of an Enum class member."""
+
+ def __init__(
+ self, enum_class: type[_ET], case_sensitive: bool = True
+ ) -> None:
+ """Initializes EnumParser.
+
+ Args:
+ enum_class: class, the Enum class with all possible flag values.
+ case_sensitive: bool, whether or not the enum is to be case-sensitive. If
+ False, all member names must be unique when case is ignored.
+
+ Raises:
+ TypeError: When enum_class is not a subclass of Enum.
+ ValueError: When enum_class is empty.
+ """
+ if not issubclass(enum_class, enum.Enum):
+ raise TypeError(f'{enum_class} is not a subclass of Enum.')
+ if not enum_class.__members__:
+ raise ValueError('enum_class cannot be empty, but "{}" is empty.'
+ .format(enum_class))
+ if not case_sensitive:
+ members = collections.Counter(
+ name.lower() for name in enum_class.__members__)
+ duplicate_keys = {
+ member for member, count in members.items() if count > 1
+ }
+ if duplicate_keys:
+ raise ValueError(
+ 'Duplicate enum values for {} using case_sensitive=False'.format(
+ duplicate_keys))
+
+ super().__init__()
+ self.enum_class = enum_class
+ self._case_sensitive = case_sensitive
+ if case_sensitive:
+ self._member_names = tuple(enum_class.__members__)
+ else:
+ self._member_names = tuple(
+ name.lower() for name in enum_class.__members__)
+
+ @property
+ def member_names(self) -> Sequence[str]:
+ """The accepted enum names, in lowercase if not case sensitive."""
+ return self._member_names
+
+ def parse(self, argument: _ET | str) -> _ET:
+ """Determines validity of argument and returns the correct element of enum.
+
+ Args:
+ argument: str or Enum class member, the supplied flag value.
+
+ Returns:
+ The first matching Enum class member in Enum class.
+
+ Raises:
+ ValueError: Raised when argument didn't match anything in enum.
+ """
+ if isinstance(argument, self.enum_class):
+ return argument # pytype: disable=bad-return-type
+ elif not isinstance(argument, str):
+ raise ValueError(
+ '{} is not an enum member or a name of a member in {}'.format(
+ argument, self.enum_class))
+ key = EnumParser(
+ self._member_names, case_sensitive=self._case_sensitive).parse(argument)
+ if self._case_sensitive:
+ return self.enum_class[key]
+ else:
+ # If EnumParser.parse() return a value, we're guaranteed to find it
+ # as a member of the class
+ return next(value for name, value in self.enum_class.__members__.items()
+ if name.lower() == key.lower())
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return 'enum class'
+
+
+class ListSerializer(Generic[_T], ArgumentSerializer[list[_T]]):
+
+ def __init__(self, list_sep: str) -> None:
+ self.list_sep = list_sep
+
+ def serialize(self, value: list[_T]) -> str:
+ """See base class."""
+ return self.list_sep.join([str(x) for x in value])
+
+
+class EnumClassListSerializer(ListSerializer[_ET]):
+ """A serializer for :class:`MultiEnumClass` flags.
+
+ This serializer simply joins the output of `EnumClassSerializer` using a
+ provided separator.
+ """
+
+ _element_serializer: 'EnumClassSerializer'
+
+ def __init__(self, list_sep: str, **kwargs) -> None:
+ """Initializes EnumClassListSerializer.
+
+ Args:
+ list_sep: String to be used as a separator when serializing
+ **kwargs: Keyword arguments to the `EnumClassSerializer` used to serialize
+ individual values.
+ """
+ super().__init__(list_sep)
+ self._element_serializer = EnumClassSerializer(**kwargs)
+
+ def serialize(self, value: _ET | list[_ET]) -> str:
+ """See base class."""
+ if isinstance(value, list):
+ return self.list_sep.join(
+ self._element_serializer.serialize(x) for x in value)
+ else:
+ return self._element_serializer.serialize(value)
+
+
+class CsvListSerializer(ListSerializer[str]):
+
+ def serialize(self, value: list[str]) -> str:
+ """Serializes a list as a CSV string or unicode."""
+ output = io.StringIO()
+ writer = csv.writer(output, delimiter=self.list_sep)
+ writer.writerow([str(x) for x in value])
+ serialized_value = output.getvalue().strip()
+
+ # We need the returned value to be pure ascii or Unicodes so that
+ # when the xml help is generated they are usefully encodable.
+ return str(serialized_value)
+
+
+class EnumClassSerializer(ArgumentSerializer[_ET]):
+ """Class for generating string representations of an enum class flag value."""
+
+ def __init__(self, lowercase: bool) -> None:
+ """Initializes EnumClassSerializer.
+
+ Args:
+ lowercase: If True, enum member names are lowercased during serialization.
+ """
+ self._lowercase = lowercase
+
+ def serialize(self, value: _ET) -> str:
+ """Returns a serialized string of the Enum class value."""
+ as_string = str(value.name)
+ return as_string.lower() if self._lowercase else as_string
+
+
+class BaseListParser(ArgumentParser):
+ """Base class for a parser of lists of strings.
+
+ To extend, inherit from this class; from the subclass ``__init__``, call::
+
+ super().__init__(token, name)
+
+ where token is a character used to tokenize, and name is a description
+ of the separator.
+ """
+
+ def __init__(self, token: str | None = None, name: str | None = None) -> None:
+ assert name
+ super().__init__()
+ self._token = token
+ self._name = name
+ self.syntactic_help = 'a %s separated list' % self._name
+
+ def parse(self, argument: str) -> list[str]:
+ """See base class."""
+ if isinstance(argument, list):
+ return argument
+ elif not argument:
+ return []
+ else:
+ return [s.strip() for s in argument.split(self._token)]
+
+ def flag_type(self) -> str:
+ """See base class."""
+ return '%s separated list of strings' % self._name
+
+
+class ListParser(BaseListParser):
+ """Parser for a comma-separated list of strings."""
+
+ def __init__(self) -> None:
+ super().__init__(',', 'comma')
+
+ def parse(self, argument: str | list[str]) -> list[str]:
+ """Parses argument as comma-separated list of strings."""
+ if isinstance(argument, list):
+ return argument
+ elif not argument:
+ return []
+ else:
+ try:
+ return [s.strip() for s in list(csv.reader([argument], strict=True))[0]]
+ except csv.Error as e:
+ # Provide a helpful report for case like
+ # --listflag="$(printf 'hello,\nworld')"
+ # IOW, list flag values containing naked newlines. This error
+ # was previously "reported" by allowing csv.Error to
+ # propagate.
+ raise ValueError('Unable to parse the value %r as a %s: %s'
+ % (argument, self.flag_type(), e))
+
+ def _custom_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = super()._custom_xml_dom_elements(doc)
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'list_separator', repr(',')))
+ return elements
+
+
+class WhitespaceSeparatedListParser(BaseListParser):
+ """Parser for a whitespace-separated list of strings."""
+
+ def __init__(self, comma_compat: bool = False) -> None:
+ """Initializer.
+
+ Args:
+ comma_compat: bool, whether to support comma as an additional separator.
+ If False then only whitespace is supported. This is intended only for
+ backwards compatibility with flags that used to be comma-separated.
+ """
+ self._comma_compat = comma_compat
+ name = 'whitespace or comma' if self._comma_compat else 'whitespace'
+ super().__init__(None, name)
+
+ def parse(self, argument: str | list[str]) -> list[str]:
+ """Parses argument as whitespace-separated list of strings.
+
+ It also parses argument as comma-separated list of strings if requested.
+
+ Args:
+ argument: string argument passed in the commandline.
+
+ Returns:
+ [str], the parsed flag value.
+ """
+ if isinstance(argument, list):
+ return argument
+ elif not argument:
+ return []
+ else:
+ if self._comma_compat:
+ argument = argument.replace(',', ' ')
+ return argument.split()
+
+ def _custom_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = super()._custom_xml_dom_elements(doc)
+ separators = list(string.whitespace)
+ if self._comma_compat:
+ separators.append(',')
+ separators.sort()
+ for sep_char in separators:
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'list_separator', repr(sep_char)))
+ return elements
diff --git a/venv/Lib/site-packages/absl/flags/_defines.py b/venv/Lib/site-packages/absl/flags/_defines.py
new file mode 100644
index 0000000000000000000000000000000000000000..4914764cdeb9a43eb8dc16b88939b129a177e9e3
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_defines.py
@@ -0,0 +1,1702 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""This modules contains flags DEFINE functions.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+from collections.abc import Iterable
+import enum
+import sys
+import types
+from typing import Any, Literal, TypeVar, overload
+
+from absl.flags import _argument_parser
+from absl.flags import _exceptions
+from absl.flags import _flag
+from absl.flags import _flagvalues
+from absl.flags import _helpers
+from absl.flags import _validators
+
+_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
+
+_T = TypeVar('_T')
+_ET = TypeVar('_ET', bound=enum.Enum)
+
+
+def _register_bounds_validator_if_needed(parser, name, flag_values):
+ """Enforces lower and upper bounds for numeric flags.
+
+ Args:
+ parser: NumericParser (either FloatParser or IntegerParser), provides lower
+ and upper bounds, and help text to display.
+ name: str, name of the flag
+ flag_values: FlagValues.
+ """
+ if parser.lower_bound is not None or parser.upper_bound is not None:
+
+ def checker(value):
+ if value is not None and parser.is_outside_bounds(value):
+ message = '%s is not %s' % (value, parser.syntactic_help)
+ raise _exceptions.ValidationError(message)
+ return True
+
+ _validators.register_validator(name, checker, flag_values=flag_values)
+
+
+@overload
+def DEFINE( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ name: str,
+ default: Any,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ serializer: _argument_parser.ArgumentSerializer[_T] | None = ...,
+ module_name: str | None = ...,
+ required: Literal[True] = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[_T]:
+ ...
+
+
+@overload
+def DEFINE( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ name: str,
+ default: Any | None,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ serializer: _argument_parser.ArgumentSerializer[_T] | None = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[_T | None]:
+ ...
+
+
+def DEFINE( # pylint: disable=invalid-name
+ parser,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ serializer=None,
+ module_name=None,
+ required=False,
+ **args):
+ """Registers a generic Flag object.
+
+ NOTE: in the docstrings of all DEFINE* functions, "registers" is short
+ for "creates a new flag and registers it".
+
+ Auxiliary function: clients should use the specialized ``DEFINE_``
+ function instead.
+
+ Args:
+ parser: :class:`ArgumentParser`, used to parse the flag arguments.
+ name: str, the flag name.
+ default: The default value of the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ serializer: :class:`ArgumentSerializer`, the flag serializer instance.
+ module_name: str, the name of the Python module declaring this flag. If not
+ provided, it will be computed using the stack trace of this call.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ return DEFINE_flag(
+ _flag.Flag(parser, serializer, name, default, help, **args),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+
+
+@overload
+def DEFINE_flag( # pylint: disable=invalid-name
+ flag: _flag.Flag[_T],
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: Literal[True] = ...,
+) -> _flagvalues.FlagHolder[_T]:
+ ...
+
+
+@overload
+def DEFINE_flag( # pylint: disable=invalid-name
+ flag: _flag.Flag[_T],
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+) -> _flagvalues.FlagHolder[_T | None]:
+ ...
+
+
+def DEFINE_flag( # pylint: disable=invalid-name
+ flag,
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ required=False):
+ """Registers a :class:`Flag` object with a :class:`FlagValues` object.
+
+ By default, the global :const:`FLAGS` ``FlagValue`` object is used.
+
+ Typical users will use one of the more specialized DEFINE_xxx
+ functions, such as :func:`DEFINE_string` or :func:`DEFINE_integer`. But
+ developers who need to create :class:`Flag` objects themselves should use
+ this function to register their flags.
+
+ Args:
+ flag: :class:`Flag`, a flag that is key to the module.
+ flag_values: :class:`FlagValues`, the ``FlagValues`` instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: str, the name of the Python module declaring this flag. If not
+ provided, it will be computed using the stack trace of this call.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+
+ Returns:
+ a handle to defined flag.
+ """
+ if required and flag.default is not None:
+ raise ValueError(
+ 'Required flag --%s needs to have None as default' % flag.name
+ )
+ # Copying the reference to flag_values prevents pychecker warnings.
+ fv = flag_values
+ fv[flag.name] = flag
+ # Tell flag_values who's defining the flag.
+ if module_name:
+ module = sys.modules.get(module_name)
+ else:
+ module, module_name = _helpers.get_calling_module_object_and_name()
+ flag_values.register_flag_by_module(module_name, flag)
+ flag_values.register_flag_by_module_id(id(module), flag)
+ if required:
+ _validators.mark_flag_as_required(flag.name, fv)
+ ensure_non_none_value = (flag.default is not None) or required
+ return _flagvalues.FlagHolder(
+ fv, flag, ensure_non_none_value=ensure_non_none_value)
+
+
+def set_default(flag_holder: _flagvalues.FlagHolder[_T], value: _T) -> None:
+ """Changes the default value of the provided flag object.
+
+ The flag's current value is also updated if the flag is currently using
+ the default value, i.e. not specified in the command line, and not set
+ by FLAGS.name = value.
+
+ Args:
+ flag_holder: FlagHolder, the flag to modify.
+ value: The new default value.
+
+ Raises:
+ IllegalFlagValueError: Raised when value is not valid.
+ """
+ flag_holder._flagvalues.set_default(flag_holder.name, value) # pylint: disable=protected-access
+
+
+def override_value(flag_holder: _flagvalues.FlagHolder[_T], value: _T) -> None:
+ """Overrides the value of the provided flag.
+
+ This value takes precedent over the default value and, when called after flag
+ parsing, any value provided at the command line.
+
+ Args:
+ flag_holder: FlagHolder, the flag to modify.
+ value: The new value.
+
+ Raises:
+ IllegalFlagValueError: The value did not pass the flag parser or validators.
+ """
+ fv = flag_holder._flagvalues # pylint: disable=protected-access
+ # Ensure the new value satisfies the flag's parser while avoiding side
+ # effects of calling parse().
+ parsed = fv[flag_holder.name]._parse(value) # pylint: disable=protected-access
+ if parsed != value:
+ raise _exceptions.IllegalFlagValueError(
+ 'flag %s: parsed value %r not equal to original %r'
+ % (flag_holder.name, parsed, value)
+ )
+ setattr(fv, flag_holder.name, value)
+
+
+def _internal_declare_key_flags(
+ flag_names: list[str],
+ flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS,
+ key_flag_values: _flagvalues.FlagValues | None = None,
+) -> None:
+ """Declares a flag as key for the calling module.
+
+ Internal function. User code should call declare_key_flag or
+ adopt_module_key_flags instead.
+
+ Args:
+ flag_names: [str], a list of names of already-registered Flag objects.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flags listed in flag_names have registered (the value of the flag_values
+ argument from the ``DEFINE_*`` calls that defined those flags). This
+ should almost never need to be overridden.
+ key_flag_values: :class:`FlagValues`, the FlagValues instance that (among
+ possibly many other things) keeps track of the key flags for each module.
+ Default ``None`` means "same as flag_values". This should almost never
+ need to be overridden.
+
+ Raises:
+ UnrecognizedFlagError: Raised when the flag is not defined.
+ """
+ key_flag_values = key_flag_values or flag_values
+
+ module = _helpers.get_calling_module()
+
+ for flag_name in flag_names:
+ key_flag_values.register_key_flag_for_module(module, flag_values[flag_name])
+
+
+def declare_key_flag(
+ flag_name: str | _flagvalues.FlagHolder,
+ flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS,
+) -> None:
+ """Declares one flag as key to the current module.
+
+ Key flags are flags that are deemed really important for a module.
+ They are important when listing help messages; e.g., if the
+ --helpshort command-line flag is used, then only the key flags of the
+ main module are listed (instead of all flags, as in the case of
+ --helpfull).
+
+ Sample usage::
+
+ flags.declare_key_flag('flag_1')
+
+ Args:
+ flag_name: str | :class:`FlagHolder`, the name or holder of an already
+ declared flag. (Redeclaring flags as key, including flags implicitly key
+ because they were declared in this module, is a no-op.)
+ Positional-only parameter.
+ flag_values: :class:`FlagValues`, the FlagValues instance in which the
+ flag will be declared as a key flag. This should almost never need to be
+ overridden.
+
+ Raises:
+ ValueError: Raised if flag_name not defined as a Python flag.
+ """
+ flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
+ if flag_name in _helpers.SPECIAL_FLAGS:
+ # Take care of the special flags, e.g., --flagfile, --undefok.
+ # These flags are defined in SPECIAL_FLAGS, and are treated
+ # specially during flag parsing, taking precedence over the
+ # user-defined flags.
+ _internal_declare_key_flags([flag_name],
+ flag_values=_helpers.SPECIAL_FLAGS,
+ key_flag_values=flag_values)
+ return
+ try:
+ _internal_declare_key_flags([flag_name], flag_values=flag_values)
+ except KeyError:
+ raise ValueError('Flag --%s is undefined. To set a flag as a key flag '
+ 'first define it in Python.' % flag_name)
+
+
+def adopt_module_key_flags(
+ module: Any, flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS
+) -> None:
+ """Declares that all flags key to a module are key to the current module.
+
+ Args:
+ module: module, the module object from which all key flags will be declared
+ as key flags to the current module.
+ flag_values: :class:`FlagValues`, the FlagValues instance in which the
+ flags will be declared as key flags. This should almost never need to be
+ overridden.
+
+ Raises:
+ Error: Raised when given an argument that is a module name (a string),
+ instead of a module object.
+ """
+ if not isinstance(module, types.ModuleType):
+ raise _exceptions.Error('Expected a module object, not %r.' % (module,))
+ _internal_declare_key_flags(
+ [f.name for f in flag_values.get_key_flags_for_module(module.__name__)],
+ flag_values=flag_values)
+ # If module is this flag module, take _helpers.SPECIAL_FLAGS into account.
+ if module == _helpers.FLAGS_MODULE:
+ _internal_declare_key_flags(
+ # As we associate flags with get_calling_module_object_and_name(), the
+ # special flags defined in this module are incorrectly registered with
+ # a different module. So, we can't use get_key_flags_for_module.
+ # Instead, we take all flags from _helpers.SPECIAL_FLAGS (a private
+ # FlagValues, where no other module should register flags).
+ [_helpers.SPECIAL_FLAGS[name].name for name in _helpers.SPECIAL_FLAGS],
+ flag_values=_helpers.SPECIAL_FLAGS,
+ key_flag_values=flag_values)
+
+
+def disclaim_key_flags() -> None:
+ """Declares that the current module will not define any more key flags.
+
+ Normally, the module that calls the DEFINE_xxx functions claims the
+ flag to be its key flag. This is undesirable for modules that
+ define additional DEFINE_yyy functions with its own flag parsers and
+ serializers, since that module will accidentally claim flags defined
+ by DEFINE_yyy as its key flags. After calling this function, the
+ module disclaims flag definitions thereafter, so the key flags will
+ be correctly attributed to the caller of DEFINE_yyy.
+
+ After calling this function, the module will not be able to define
+ any more flags. This function will affect all FlagValues objects.
+ """
+ globals_for_caller = sys._getframe(1).f_globals # pylint: disable=protected-access
+ module = _helpers.get_module_object_and_name(globals_for_caller)
+ if module is not None:
+ _helpers.disclaim_module_ids.add(id(module.module))
+
+
+@overload
+def DEFINE_string( # pylint: disable=invalid-name
+ name: str,
+ default: str | None,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[str]:
+ ...
+
+
+@overload
+def DEFINE_string( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[str | None]:
+ ...
+
+
+@overload
+def DEFINE_string( # pylint: disable=invalid-name
+ name: str,
+ default: str,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[str]:
+ ...
+
+
+def DEFINE_string( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be any string."""
+ parser = _argument_parser.ArgumentParser[str]()
+ serializer = _argument_parser.ArgumentSerializer[str]()
+ return DEFINE(
+ parser,
+ name,
+ default,
+ help,
+ flag_values,
+ serializer,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_boolean( # pylint: disable=invalid-name
+ name: str,
+ default: None | str | bool | int,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[bool]:
+ ...
+
+
+@overload
+def DEFINE_boolean( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[bool | None]:
+ ...
+
+
+@overload
+def DEFINE_boolean( # pylint: disable=invalid-name
+ name: str,
+ default: str | bool | int,
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[bool]:
+ ...
+
+
+def DEFINE_boolean( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ required=False,
+ **args
+):
+ """Registers a boolean flag.
+
+ Such a boolean flag does not take an argument. If a user wants to
+ specify a false value explicitly, the long option beginning with 'no'
+ must be used: i.e. --noflag
+
+ This flag will have a value of None, True or False. None is possible
+ if default=None and the user does not specify the flag on the command
+ line.
+
+ Args:
+ name: str, the flag name.
+ default: bool|str|None, the default value of the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: str, the name of the Python module declaring this flag. If not
+ provided, it will be computed using the stack trace of this call.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ return DEFINE_flag( # pytype: disable=bad-return-type
+ _flag.BooleanFlag(name, default, help, **args),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+
+
+@overload
+def DEFINE_float( # pylint: disable=invalid-name
+ name: str,
+ default: None | float | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[float]:
+ ...
+
+
+@overload
+def DEFINE_float( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[float | None]:
+ ...
+
+
+@overload
+def DEFINE_float( # pylint: disable=invalid-name
+ name: str,
+ default: float | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[float]:
+ ...
+
+
+def DEFINE_float( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ lower_bound=None,
+ upper_bound=None,
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value must be a float.
+
+ If ``lower_bound`` or ``upper_bound`` are set, then this flag must be
+ within the given range.
+
+ Args:
+ name: str, the flag name.
+ default: float|str|None, the default value of the flag.
+ help: str, the help message.
+ lower_bound: float, min value of the flag.
+ upper_bound: float, max value of the flag.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to :func:`DEFINE`.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.FloatParser(lower_bound, upper_bound)
+ serializer = _argument_parser.ArgumentSerializer()
+ result = DEFINE(
+ parser,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values,
+ serializer,
+ required=True if required else False,
+ **args,
+ )
+ _register_bounds_validator_if_needed(parser, name, flag_values=flag_values)
+ return result
+
+
+@overload
+def DEFINE_integer( # pylint: disable=invalid-name
+ name: str,
+ default: None | int | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[int]:
+ ...
+
+
+@overload
+def DEFINE_integer( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[int | None]:
+ ...
+
+
+@overload
+def DEFINE_integer( # pylint: disable=invalid-name
+ name: str,
+ default: int | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[int]:
+ ...
+
+
+def DEFINE_integer( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ lower_bound=None,
+ upper_bound=None,
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value must be an integer.
+
+ If ``lower_bound``, or ``upper_bound`` are set, then this flag must be
+ within the given range.
+
+ Args:
+ name: str, the flag name.
+ default: int|str|None, the default value of the flag.
+ help: str, the help message.
+ lower_bound: int, min value of the flag.
+ upper_bound: int, max value of the flag.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to :func:`DEFINE`.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.IntegerParser(lower_bound, upper_bound)
+ serializer = _argument_parser.ArgumentSerializer()
+ result = DEFINE(
+ parser,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values,
+ serializer,
+ required=True if required else False,
+ **args,
+ )
+ _register_bounds_validator_if_needed(parser, name, flag_values=flag_values)
+ return result
+
+
+@overload
+def DEFINE_enum( # pylint: disable=invalid-name
+ name: str,
+ default: str | None,
+ enum_values: Iterable[str],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[str]:
+ ...
+
+
+@overload
+def DEFINE_enum( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ enum_values: Iterable[str],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[str | None]:
+ ...
+
+
+@overload
+def DEFINE_enum( # pylint: disable=invalid-name
+ name: str,
+ default: str,
+ enum_values: Iterable[str],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[str]:
+ ...
+
+
+def DEFINE_enum( # pylint: disable=invalid-name
+ name,
+ default,
+ enum_values,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be any string from enum_values.
+
+ Instead of a string enum, prefer `DEFINE_enum_class`, which allows
+ defining enums from an `enum.Enum` class.
+
+ Args:
+ name: str, the flag name.
+ default: str|None, the default value of the flag.
+ enum_values: [str], a non-empty list of strings with the possible values for
+ the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: str, the name of the Python module declaring this flag. If not
+ provided, it will be computed using the stack trace of this call.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ result = DEFINE_flag(
+ _flag.EnumFlag(name, default, help, enum_values, **args),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+ return result
+
+
+@overload
+def DEFINE_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: None | _ET | str,
+ enum_class: type[_ET],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ case_sensitive: bool = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[_ET]:
+ ...
+
+
+@overload
+def DEFINE_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ enum_class: type[_ET],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ case_sensitive: bool = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[_ET | None]:
+ ...
+
+
+@overload
+def DEFINE_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: _ET | str,
+ enum_class: type[_ET],
+ help: str | None, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ case_sensitive: bool = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[_ET]:
+ ...
+
+
+def DEFINE_enum_class( # pylint: disable=invalid-name
+ name,
+ default,
+ enum_class,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ case_sensitive=False,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be the name of enum members.
+
+ Args:
+ name: str, the flag name.
+ default: Enum|str|None, the default value of the flag.
+ enum_class: class, the Enum class with all the possible values for the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: str, the name of the Python module declaring this flag. If not
+ provided, it will be computed using the stack trace of this call.
+ case_sensitive: bool, whether to map strings to members of the enum_class
+ without considering case.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: dict, the extra keyword args that are passed to ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ # NOTE: pytype fails if this is a direct return.
+ result = DEFINE_flag(
+ _flag.EnumClassFlag(
+ name, default, help, enum_class, case_sensitive=case_sensitive, **args
+ ),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+ return result
+
+
+@overload
+def DEFINE_list( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+@overload
+def DEFINE_list( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str] | None]:
+ ...
+
+
+@overload
+def DEFINE_list( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+def DEFINE_list( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value is a comma-separated list of strings.
+
+ The flag value is parsed with a CSV parser.
+
+ Args:
+ name: str, the flag name.
+ default: list|str|None, the default value of the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.ListParser()
+ serializer = _argument_parser.CsvListSerializer(',')
+ return DEFINE(
+ parser,
+ name,
+ default,
+ help,
+ flag_values,
+ serializer,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_spaceseplist( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ comma_compat: bool = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+@overload
+def DEFINE_spaceseplist( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ comma_compat: bool = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str] | None]:
+ ...
+
+
+@overload
+def DEFINE_spaceseplist( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ comma_compat: bool = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+def DEFINE_spaceseplist( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ comma_compat=False,
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value is a whitespace-separated list of strings.
+
+ Any whitespace can be used as a separator.
+
+ Args:
+ name: str, the flag name.
+ default: list|str|None, the default value of the flag.
+ help: str, the help message.
+ comma_compat: bool - Whether to support comma as an additional separator. If
+ false then only whitespace is supported. This is intended only for
+ backwards compatibility with flags that used to be comma-separated.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.WhitespaceSeparatedListParser(
+ comma_compat=comma_compat)
+ serializer = _argument_parser.ListSerializer(' ')
+ return DEFINE(
+ parser,
+ name,
+ default,
+ help,
+ flag_values,
+ serializer,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T],
+ name: str,
+ default: Iterable[_T],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_T]]:
+ ...
+
+
+@overload
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T],
+ name: str,
+ default: None | _T,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_T]]:
+ ...
+
+
+@overload
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T],
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_T] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T],
+ name: str,
+ default: Iterable[_T],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_T]]:
+ ...
+
+
+@overload
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T],
+ name: str,
+ default: _T,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_T]]:
+ ...
+
+
+def DEFINE_multi( # pylint: disable=invalid-name
+ parser,
+ serializer,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ required=False,
+ **args
+):
+ """Registers a generic MultiFlag that parses its args with a given parser.
+
+ Auxiliary function. Normal users should NOT use it directly.
+
+ Developers who need to create their own 'Parser' classes for options
+ which can appear multiple times can call this module function to
+ register their flags.
+
+ Args:
+ parser: ArgumentParser, used to parse the flag arguments.
+ serializer: ArgumentSerializer, the flag serializer instance.
+ name: str, the flag name.
+ default: Union[Iterable[T], str, None], the default value of the flag. If
+ the value is text, it will be parsed as if it was provided from the
+ command line. If the value is a non-string iterable, it will be iterated
+ over to create a shallow copy of the values. If it is None, it is left
+ as-is.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: A string, the name of the Python module declaring this flag. If
+ not provided, it will be computed using the stack trace of this call.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ result = DEFINE_flag(
+ _flag.MultiFlag(parser, serializer, name, default, help, **args),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+ return result
+
+
+@overload
+def DEFINE_multi_string( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+@overload
+def DEFINE_multi_string( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi_string( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[str] | str,
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+def DEFINE_multi_string( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be a list of any strings.
+
+ Use the flag on the command line multiple times to place multiple
+ string values into the list. The 'default' may be a single string
+ (which will be converted into a single-element list) or a list of
+ strings.
+
+
+ Args:
+ name: str, the flag name.
+ default: Union[Iterable[str], str, None], the default value of the flag; see
+ :func:`DEFINE_multi`.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.ArgumentParser()
+ serializer = _argument_parser.ArgumentSerializer()
+ return DEFINE_multi(
+ parser,
+ serializer,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_multi_integer( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[int] | int | str,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[int]]:
+ ...
+
+
+@overload
+def DEFINE_multi_integer( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[int] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi_integer( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[int] | int | str,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: int | None = ...,
+ upper_bound: int | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[int]]:
+ ...
+
+
+def DEFINE_multi_integer( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ lower_bound=None,
+ upper_bound=None,
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be a list of arbitrary integers.
+
+ Use the flag on the command line multiple times to place multiple
+ integer values into the list. The 'default' may be a single integer
+ (which will be converted into a single-element list) or a list of
+ integers.
+
+ Args:
+ name: str, the flag name.
+ default: Union[Iterable[int], str, None], the default value of the flag; see
+ `DEFINE_multi`.
+ help: str, the help message.
+ lower_bound: int, min values of the flag.
+ upper_bound: int, max values of the flag.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.IntegerParser(lower_bound, upper_bound)
+ serializer = _argument_parser.ArgumentSerializer()
+ return DEFINE_multi(
+ parser,
+ serializer,
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ flag_values,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_multi_float( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[float] | float | str,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[float]]:
+ ...
+
+
+@overload
+def DEFINE_multi_float( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[float] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi_float( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[float] | float | str,
+ help: str, # pylint: disable=redefined-builtin
+ lower_bound: float | None = ...,
+ upper_bound: float | None = ...,
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[float]]:
+ ...
+
+
+def DEFINE_multi_float( # pylint: disable=invalid-name
+ name,
+ default,
+ help, # pylint: disable=redefined-builtin
+ lower_bound=None,
+ upper_bound=None,
+ flag_values=_flagvalues.FLAGS,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be a list of arbitrary floats.
+
+ Use the flag on the command line multiple times to place multiple
+ float values into the list. The 'default' may be a single float
+ (which will be converted into a single-element list) or a list of
+ floats.
+
+ Args:
+ name: str, the flag name.
+ default: Union[Iterable[float], str, None], the default value of the flag;
+ see `DEFINE_multi`.
+ help: str, the help message.
+ lower_bound: float, min values of the flag.
+ upper_bound: float, max values of the flag.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.FloatParser(lower_bound, upper_bound)
+ serializer = _argument_parser.ArgumentSerializer()
+ return DEFINE_multi(
+ parser,
+ serializer,
+ name,
+ default,
+ help,
+ flag_values,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_multi_enum( # pylint: disable=invalid-name
+ name: str,
+ default: None | Iterable[str] | str,
+ enum_values: Iterable[str],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ enum_values: Iterable[str],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum( # pylint: disable=invalid-name
+ name: str,
+ default: Iterable[str] | str,
+ enum_values: Iterable[str],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[str]]:
+ ...
+
+
+def DEFINE_multi_enum( # pylint: disable=invalid-name
+ name,
+ default,
+ enum_values,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ case_sensitive=True,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be a list strings from enum_values.
+
+ Use the flag on the command line multiple times to place multiple
+ enum values into the list. The 'default' may be a single string
+ (which will be converted into a single-element list) or a list of
+ strings.
+
+ Args:
+ name: str, the flag name.
+ default: Union[Iterable[str], str, None], the default value of the flag; see
+ `DEFINE_multi`.
+ enum_values: [str], a non-empty list of strings with the possible values for
+ the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ case_sensitive: Whether or not the enum is to be case-sensitive.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ parser = _argument_parser.EnumParser(enum_values, case_sensitive)
+ serializer = _argument_parser.ArgumentSerializer()
+ return DEFINE_multi(
+ parser,
+ serializer,
+ name,
+ default,
+ '<%s>: %s' % ('|'.join(enum_values), help),
+ flag_values,
+ required=True if required else False,
+ **args,
+ )
+
+
+@overload
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name: str,
+ # This is separate from `Union[None, _ET, Iterable[str], str]` to avoid a
+ # Pytype issue inferring the return value to
+ # FlagHolder[List[Union[_ET, enum.Enum]]] when an iterable of concrete enum
+ # subclasses are used.
+ default: Iterable[_ET],
+ enum_class: type[_ET],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_ET]]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: None | _ET | Iterable[str] | str,
+ enum_class: type[_ET],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ *,
+ required: Literal[True],
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_ET]]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: None,
+ enum_class: type[_ET],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_ET] | None]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name: str,
+ # This is separate from `Union[None, _ET, Iterable[str], str]` to avoid a
+ # Pytype issue inferring the return value to
+ # FlagHolder[List[Union[_ET, enum.Enum]]] when an iterable of concrete enum
+ # subclasses are used.
+ default: Iterable[_ET],
+ enum_class: type[_ET],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_ET]]:
+ ...
+
+
+@overload
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name: str,
+ default: _ET | Iterable[str] | str,
+ enum_class: type[_ET],
+ help: str, # pylint: disable=redefined-builtin
+ flag_values: _flagvalues.FlagValues = ...,
+ module_name: str | None = ...,
+ required: bool = ...,
+ **args: Any
+) -> _flagvalues.FlagHolder[list[_ET]]:
+ ...
+
+
+def DEFINE_multi_enum_class( # pylint: disable=invalid-name
+ name,
+ default,
+ enum_class,
+ help, # pylint: disable=redefined-builtin
+ flag_values=_flagvalues.FLAGS,
+ module_name=None,
+ case_sensitive=False,
+ required=False,
+ **args
+):
+ """Registers a flag whose value can be a list of enum members.
+
+ Use the flag on the command line multiple times to place multiple
+ enum values into the list.
+
+ Args:
+ name: str, the flag name.
+ default: Union[Iterable[Enum], Iterable[str], Enum, str, None], the default
+ value of the flag; see `DEFINE_multi`; only differences are documented
+ here. If the value is a single Enum, it is treated as a single-item list
+ of that Enum value. If it is an iterable, text values within the iterable
+ will be converted to the equivalent Enum objects.
+ enum_class: class, the Enum class with all the possible values for the flag.
+ help: str, the help message.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: A string, the name of the Python module declaring this flag. If
+ not provided, it will be computed using the stack trace of this call.
+ case_sensitive: bool, whether to map strings to members of the enum_class
+ without considering case.
+ required: bool, is this a required flag. This must be used as a keyword
+ argument.
+ **args: Dictionary with extra keyword args that are passed to the
+ ``Flag.__init__``.
+
+ Returns:
+ a handle to defined flag.
+ """
+ # NOTE: pytype fails if this is a direct return.
+ result = DEFINE_flag(
+ _flag.MultiEnumClassFlag(
+ name,
+ default,
+ help,
+ enum_class,
+ case_sensitive=case_sensitive,
+ **args,
+ ),
+ flag_values,
+ module_name,
+ required=True if required else False,
+ )
+ return result
+
+
+def DEFINE_alias( # pylint: disable=invalid-name
+ name: str,
+ original_name: str,
+ flag_values: _flagvalues.FlagValues = _flagvalues.FLAGS,
+ module_name: str | None = None,
+) -> _flagvalues.FlagHolder[Any]:
+ """Defines an alias flag for an existing one.
+
+ Args:
+ name: str, the flag name.
+ original_name: str, the original flag name.
+ flag_values: :class:`FlagValues`, the FlagValues instance with which the
+ flag will be registered. This should almost never need to be overridden.
+ module_name: A string, the name of the module that defines this flag.
+
+ Returns:
+ a handle to defined flag.
+
+ Raises:
+ flags.FlagError:
+ UnrecognizedFlagError: if the referenced flag doesn't exist.
+ DuplicateFlagError: if the alias name has been used by some existing flag.
+ """
+ if original_name not in flag_values:
+ raise _exceptions.UnrecognizedFlagError(original_name)
+ flag = flag_values[original_name]
+
+ class _FlagAlias(_flag.Flag):
+ """Overrides Flag class so alias value is copy of original flag value."""
+
+ def parse(self, argument):
+ flag.parse(argument)
+ self.present += 1
+
+ def _parse_from_default(self, value):
+ # The value was already parsed by the aliased flag, so there is no
+ # need to call the parser on it a second time.
+ # Additionally, because of how MultiFlag parses and merges values,
+ # it isn't possible to delegate to the aliased flag and still get
+ # the correct values.
+ return value
+
+ @property
+ def value(self):
+ return flag.value
+
+ @value.setter
+ def value(self, value):
+ flag.value = value
+
+ help_msg = 'Alias for --%s.' % flag.name
+ # If alias_name has been used, flags.DuplicatedFlag will be raised.
+ return DEFINE_flag(
+ _FlagAlias(
+ flag.parser,
+ flag.serializer,
+ name,
+ flag.default,
+ help_msg,
+ boolean=flag.boolean), flag_values, module_name)
diff --git a/venv/Lib/site-packages/absl/flags/_exceptions.py b/venv/Lib/site-packages/absl/flags/_exceptions.py
new file mode 100644
index 0000000000000000000000000000000000000000..e9edfce684a7ea351be35eb96b32c441b2ccda2a
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_exceptions.py
@@ -0,0 +1,107 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Exception classes in ABSL flags library.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+import sys
+
+from absl.flags import _helpers
+
+
+_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
+
+
+class Error(Exception):
+ """The base class for all flags errors."""
+
+
+class CantOpenFlagFileError(Error):
+ """Raised when flagfile fails to open.
+
+ E.g. the file doesn't exist, or has wrong permissions.
+ """
+
+
+class DuplicateFlagError(Error):
+ """Raised if there is a flag naming conflict."""
+
+ @classmethod
+ def from_flag(cls, flagname, flag_values, other_flag_values=None):
+ """Creates a DuplicateFlagError by providing flag name and values.
+
+ Args:
+ flagname: str, the name of the flag being redefined.
+ flag_values: :class:`FlagValues`, the FlagValues instance containing the
+ first definition of flagname.
+ other_flag_values: :class:`FlagValues`, if it is not None, it should be
+ the FlagValues object where the second definition of flagname occurs.
+ If it is None, we assume that we're being called when attempting to
+ create the flag a second time, and we use the module calling this one
+ as the source of the second definition.
+
+ Returns:
+ An instance of DuplicateFlagError.
+ """
+ first_module = flag_values.find_module_defining_flag(
+ flagname, default='')
+ if other_flag_values is None:
+ second_module = _helpers.get_calling_module()
+ else:
+ second_module = other_flag_values.find_module_defining_flag(
+ flagname, default='')
+ flag_summary = flag_values[flagname].help
+ msg = ("The flag '%s' is defined twice. First from %s, Second from %s. "
+ "Description from first occurrence: %s") % (
+ flagname, first_module, second_module, flag_summary)
+ return cls(msg)
+
+
+class IllegalFlagValueError(Error):
+ """Raised when the flag command line argument is illegal."""
+
+
+class UnrecognizedFlagError(Error):
+ """Raised when a flag is unrecognized.
+
+ Attributes:
+ flagname: str, the name of the unrecognized flag.
+ flagvalue: The value of the flag, empty if the flag is not defined.
+ """
+
+ def __init__(self, flagname, flagvalue='', suggestions=None):
+ self.flagname = flagname
+ self.flagvalue = flagvalue
+ if suggestions:
+ # Space before the question mark is intentional to not include it in the
+ # selection when copy-pasting the suggestion from (some) terminals.
+ tip = '. Did you mean: %s ?' % ', '.join(suggestions)
+ else:
+ tip = ''
+ super().__init__("Unknown command line flag '%s'%s" % (flagname, tip))
+
+
+class UnparsedFlagAccessError(Error):
+ """Raised when accessing the flag value from unparsed :class:`FlagValues`."""
+
+
+class ValidationError(Error):
+ """Raised when flag validator constraint is not satisfied."""
+
+
+class FlagNameConflictsWithMethodError(Error):
+ """Raised when a flag name conflicts with :class:`FlagValues` methods."""
diff --git a/venv/Lib/site-packages/absl/flags/_flag.py b/venv/Lib/site-packages/absl/flags/_flag.py
new file mode 100644
index 0000000000000000000000000000000000000000..21c10ef4e9fb4355cf0616c8d804e7a3c445bac6
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_flag.py
@@ -0,0 +1,566 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Contains Flag class - information about single command-line flag.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+from collections.abc import Iterable
+import copy
+import enum
+import functools
+from typing import Any, Generic, TypeVar
+from xml.dom import minidom
+
+from absl.flags import _argument_parser
+from absl.flags import _exceptions
+from absl.flags import _helpers
+
+_T = TypeVar('_T')
+_ET = TypeVar('_ET', bound=enum.Enum)
+
+
+@functools.total_ordering
+class Flag(Generic[_T]):
+ """Information about a command-line flag.
+
+ Attributes:
+ name: the name for this flag
+ default: the default value for this flag
+ default_unparsed: the unparsed default value for this flag.
+ default_as_str: default value as repr'd string, e.g., "'true'"
+ (or None)
+ value: the most recent parsed value of this flag set by :meth:`parse`
+ help: a help string or None if no help is available
+ short_name: the single letter alias for this flag (or None)
+ boolean: if 'true', this flag does not accept arguments
+ present: true if this flag was parsed from command line flags
+ parser: an :class:`~absl.flags.ArgumentParser` object
+ serializer: an ArgumentSerializer object
+ allow_override: the flag may be redefined without raising an error,
+ and newly defined flag overrides the old one.
+ allow_override_cpp: use the flag from C++ if available the flag
+ definition is replaced by the C++ flag after init
+ allow_hide_cpp: use the Python flag despite having a C++ flag with
+ the same name (ignore the C++ flag)
+ using_default_value: the flag value has not been set by user
+ allow_overwrite: the flag may be parsed more than once without
+ raising an error, the last set value will be used
+ allow_using_method_names: whether this flag can be defined even if
+ it has a name that conflicts with a FlagValues method.
+ validators: list of the flag validators.
+
+ The only public method of a ``Flag`` object is :meth:`parse`, but it is
+ typically only called by a :class:`~absl.flags.FlagValues` object. The
+ :meth:`parse` method is a thin wrapper around the
+ :meth:`ArgumentParser.parse()` method. The
+ parsed value is saved in ``.value``, and the ``.present`` attribute is
+ updated. If this flag was already present, an Error is raised.
+
+ :meth:`parse` is also called during ``__init__`` to parse the default value
+ and initialize the ``.value`` attribute. This enables other python modules to
+ safely use flags even if the ``__main__`` module neglects to parse the
+ command line arguments. The ``.present`` attribute is cleared after
+ ``__init__`` parsing. If the default value is set to ``None``, then the
+ ``__init__`` parsing step is skipped and the ``.value`` attribute is
+ initialized to None.
+
+ Note: The default value is also presented to the user in the help
+ string, so it is important that it be a legal value for this flag.
+ """
+
+ # NOTE: pytype doesn't find defaults without this.
+ default: _T | None
+ default_as_str: str | None
+ default_unparsed: _T | None | str
+
+ parser: _argument_parser.ArgumentParser[_T]
+
+ def __init__(
+ self,
+ parser: _argument_parser.ArgumentParser[_T],
+ serializer: _argument_parser.ArgumentSerializer[_T] | None,
+ name: str,
+ default: _T | None | str,
+ help_string: str | None,
+ short_name: str | None = None,
+ boolean: bool = False,
+ allow_override: bool = False,
+ allow_override_cpp: bool = False,
+ allow_hide_cpp: bool = False,
+ allow_overwrite: bool = True,
+ allow_using_method_names: bool = False,
+ ) -> None:
+ self.name = name
+
+ if not help_string:
+ help_string = '(no help available)'
+
+ self.help = help_string
+ self.short_name = short_name
+ self.boolean = boolean
+ self.present = 0
+ self.parser = parser # type: ignore[annotation-type-mismatch]
+ self.serializer = serializer
+ self.allow_override = allow_override
+ self.allow_override_cpp = allow_override_cpp
+ self.allow_hide_cpp = allow_hide_cpp
+ self.allow_overwrite = allow_overwrite
+ self.allow_using_method_names = allow_using_method_names
+
+ self.using_default_value = True
+ self._value: _T | None = None
+ self.validators: list[Any] = []
+ if self.allow_hide_cpp and self.allow_override_cpp:
+ raise _exceptions.Error(
+ "Can't have both allow_hide_cpp (means use Python flag) and "
+ 'allow_override_cpp (means use C++ flag after InitGoogle)')
+
+ self._set_default(default)
+
+ @property
+ def value(self) -> _T | None:
+ return self._value
+
+ @value.setter
+ def value(self, value: _T | None):
+ self._value = value
+
+ def __hash__(self):
+ return hash(id(self))
+
+ def __eq__(self, other):
+ return self is other
+
+ def __lt__(self, other):
+ if isinstance(other, Flag):
+ return id(self) < id(other)
+ return NotImplemented
+
+ def __bool__(self):
+ raise TypeError('A Flag instance would always be True. '
+ 'Did you mean to test the `.value` attribute?')
+
+ def __getstate__(self):
+ raise TypeError("can't pickle Flag objects")
+
+ def __copy__(self):
+ raise TypeError('%s does not support shallow copies. '
+ 'Use copy.deepcopy instead.' % type(self).__name__)
+
+ def __deepcopy__(self, memo: dict[int, Any]) -> 'Flag[_T]':
+ result = object.__new__(type(self))
+ result.__dict__ = copy.deepcopy(self.__dict__, memo)
+ return result
+
+ def _get_parsed_value_as_string(self, value: _T | None) -> str | None:
+ """Returns parsed flag value as string."""
+ if value is None:
+ return None
+ if self.serializer:
+ return repr(self.serializer.serialize(value))
+ if self.boolean:
+ if value:
+ return repr('true')
+ else:
+ return repr('false')
+ return repr(str(value))
+
+ def parse(self, argument: str | _T) -> None:
+ """Parses string and sets flag value.
+
+ Args:
+ argument: str or the correct flag value type, argument to be parsed.
+ """
+ if self.present and not self.allow_overwrite:
+ raise _exceptions.IllegalFlagValueError(
+ 'flag --%s=%s: already defined as %s' % (
+ self.name, argument, self.value))
+ self.value = self._parse(argument)
+ self.present += 1
+
+ def _parse(self, argument: str | _T) -> _T | None:
+ """Internal parse function.
+
+ It returns the parsed value, and does not modify class states.
+
+ Args:
+ argument: str or the correct flag value type, argument to be parsed.
+
+ Returns:
+ The parsed value.
+ """
+ try:
+ return self.parser.parse(argument) # type: ignore[arg-type]
+ except (TypeError, ValueError, OverflowError) as e:
+ # Recast as IllegalFlagValueError.
+ raise _exceptions.IllegalFlagValueError(
+ 'flag --%s=%s: %s' % (self.name, argument, e))
+
+ def unparse(self) -> None:
+ self.value = self.default
+ self.using_default_value = True
+ self.present = 0
+
+ def serialize(self) -> str:
+ """Serializes the flag."""
+ return self._serialize(self.value)
+
+ def _serialize(self, value: _T | None) -> str:
+ """Internal serialize function."""
+ if value is None:
+ return ''
+ if self.boolean:
+ if value:
+ return '--%s' % self.name
+ else:
+ return '--no%s' % self.name
+ else:
+ if not self.serializer:
+ raise _exceptions.Error(
+ 'Serializer not present for flag %s' % self.name)
+ return '--%s=%s' % (self.name, self.serializer.serialize(value))
+
+ def _set_default(self, value: _T | None | str) -> None:
+ """Changes the default value (and current value too) for this Flag."""
+ self.default_unparsed = value
+ if value is None:
+ self.default = None
+ else:
+ self.default = self._parse_from_default(value)
+ self.default_as_str = self._get_parsed_value_as_string(self.default)
+ if self.using_default_value:
+ self.value = self.default
+
+ # This is split out so that aliases can skip regular parsing of the default
+ # value.
+ def _parse_from_default(self, value: str | _T) -> _T | None:
+ return self._parse(value)
+
+ def flag_type(self) -> str:
+ """Returns a str that describes the type of the flag.
+
+ NOTE: we use strings, and not the types.*Type constants because
+ our flags can have more exotic types, e.g., 'comma separated list
+ of strings', 'whitespace separated list of strings', etc.
+ """
+ return self.parser.flag_type()
+
+ def _create_xml_dom_element(
+ self, doc: minidom.Document, module_name: str, is_key: bool = False
+ ) -> minidom.Element:
+ """Returns an XML element that contains this flag's information.
+
+ This is information that is relevant to all flags (e.g., name,
+ meaning, etc.). If you defined a flag that has some other pieces of
+ info, then please override _ExtraXMLInfo.
+
+ Please do NOT override this method.
+
+ Args:
+ doc: minidom.Document, the DOM document it should create nodes from.
+ module_name: str,, the name of the module that defines this flag.
+ is_key: boolean, True iff this flag is key for main module.
+
+ Returns:
+ A minidom.Element instance.
+ """
+ element = doc.createElement('flag')
+ if is_key:
+ element.appendChild(_helpers.create_xml_dom_element(doc, 'key', 'yes'))
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'file', module_name))
+ # Adds flag features that are relevant for all flags.
+ element.appendChild(_helpers.create_xml_dom_element(doc, 'name', self.name))
+ if self.short_name:
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'short_name', self.short_name))
+ if self.help:
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'meaning', self.help))
+ # The default flag value can either be represented as a string like on the
+ # command line, or as a Python object. We serialize this value in the
+ # latter case in order to remain consistent.
+ if self.serializer and not isinstance(self.default, str):
+ if self.default is not None:
+ default_serialized = self.serializer.serialize(self.default)
+ else:
+ default_serialized = ''
+ else:
+ default_serialized = self.default # type: ignore[assignment]
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'default', default_serialized))
+ value_serialized = self._serialize_value_for_xml(self.value)
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'current', value_serialized))
+ element.appendChild(_helpers.create_xml_dom_element(
+ doc, 'type', self.flag_type()))
+ # Adds extra flag features this flag may have.
+ for e in self._extra_xml_dom_elements(doc):
+ element.appendChild(e)
+ return element
+
+ def _serialize_value_for_xml(self, value: _T | None) -> Any:
+ """Returns the serialized value, for use in an XML help text."""
+ return value
+
+ def _extra_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ """Returns extra info about this flag in XML.
+
+ "Extra" means "not already included by _create_xml_dom_element above."
+
+ Args:
+ doc: minidom.Document, the DOM document it should create nodes from.
+
+ Returns:
+ A list of minidom.Element.
+ """
+ # Usually, the parser knows the extra details about the flag, so
+ # we just forward the call to it.
+ return self.parser._custom_xml_dom_elements(doc) # pylint: disable=protected-access
+
+
+class BooleanFlag(Flag[bool]):
+ """Basic boolean flag.
+
+ Boolean flags do not take any arguments, and their value is either
+ ``True`` (1) or ``False`` (0). The false value is specified on the command
+ line by prepending the word ``'no'`` to either the long or the short flag
+ name.
+
+ For example, if a Boolean flag was created whose long name was
+ ``'update'`` and whose short name was ``'x'``, then this flag could be
+ explicitly unset through either ``--noupdate`` or ``--nox``.
+ """
+
+ def __init__(
+ self,
+ name: str,
+ default: bool | None | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ short_name: str | None = None,
+ **args
+ ) -> None:
+ p = _argument_parser.BooleanParser()
+ super().__init__(p, None, name, default, help, short_name, True, **args)
+
+
+class EnumFlag(Flag[str]):
+ """Basic enum flag; its value can be any string from list of enum_values."""
+
+ parser: _argument_parser.EnumParser
+
+ def __init__(
+ self,
+ name: str,
+ default: str | None,
+ help: str | None, # pylint: disable=redefined-builtin
+ enum_values: Iterable[str],
+ short_name: str | None = None,
+ case_sensitive: bool = True,
+ **args
+ ):
+ p = _argument_parser.EnumParser(enum_values, case_sensitive)
+ g: _argument_parser.ArgumentSerializer[str]
+ g = _argument_parser.ArgumentSerializer()
+ super().__init__(p, g, name, default, help, short_name, **args)
+ self.parser = p
+ self.help = '<%s>: %s' % ('|'.join(p.enum_values), self.help)
+
+ def _extra_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = []
+ for enum_value in self.parser.enum_values:
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'enum_value', enum_value))
+ return elements
+
+
+class EnumClassFlag(Flag[_ET]):
+ """Basic enum flag; its value is an enum class's member."""
+
+ parser: _argument_parser.EnumClassParser
+
+ def __init__(
+ self,
+ name: str,
+ default: _ET | None | str,
+ help: str | None, # pylint: disable=redefined-builtin
+ enum_class: type[_ET],
+ short_name: str | None = None,
+ case_sensitive: bool = False,
+ **args
+ ):
+ p = _argument_parser.EnumClassParser(
+ enum_class, case_sensitive=case_sensitive
+ )
+ g: _argument_parser.EnumClassSerializer[_ET]
+ g = _argument_parser.EnumClassSerializer(lowercase=not case_sensitive)
+ super().__init__(p, g, name, default, help, short_name, **args)
+ self.parser = p
+ self.help = '<%s>: %s' % ('|'.join(p.member_names), self.help)
+
+ def _extra_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = []
+ for enum_value in self.parser.enum_class.__members__.keys():
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'enum_value', enum_value))
+ return elements
+
+
+class MultiFlag(Generic[_T], Flag[list[_T]]):
+ """A flag that can appear multiple time on the command-line.
+
+ The value of such a flag is a list that contains the individual values
+ from all the appearances of that flag on the command-line.
+
+ See the __doc__ for Flag for most behavior of this class. Only
+ differences in behavior are described here:
+
+ * The default value may be either a single value or an iterable of values.
+ A single value is transformed into a single-item list of that value.
+
+ * The value of the flag is always a list, even if the option was
+ only supplied once, and even if the default value is a single
+ value
+ """
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.help += ';\n repeat this option to specify a list of values'
+
+ def parse(self, arguments: str | _T | Iterable[_T]): # pylint: disable=arguments-renamed
+ """Parses one or more arguments with the installed parser.
+
+ Args:
+ arguments: a single argument or a list of arguments (typically a
+ list of default values); a single argument is converted
+ internally into a list containing one item.
+ """
+ new_values = self._parse(arguments)
+ if self.present:
+ assert self.value is not None
+ self.value.extend(new_values)
+ else:
+ self.value = new_values
+ self.present += len(new_values)
+
+ def _parse(self, arguments: str | _T | Iterable[_T]) -> list[_T]: # pylint: disable=arguments-renamed
+ arguments_list: list[str | _T]
+
+ if isinstance(arguments, str):
+ arguments_list = [arguments]
+
+ elif isinstance(arguments, Iterable):
+ arguments_list = list(arguments)
+
+ else:
+ # Default value may be a list of values. Most other arguments
+ # will not be, so convert them into a single-item list to make
+ # processing simpler below.
+ arguments_list = [arguments]
+
+ return [super(MultiFlag, self)._parse(item) for item in arguments_list] # type: ignore
+
+ def _serialize(self, value: list[_T] | None) -> str:
+ """See base class."""
+ if not self.serializer:
+ raise _exceptions.Error(
+ 'Serializer not present for flag %s' % self.name)
+ if value is None:
+ return ''
+
+ serialized_items = [
+ super(MultiFlag, self)._serialize(value_item) # type: ignore[arg-type]
+ for value_item in value
+ ]
+
+ return '\n'.join(serialized_items)
+
+ def flag_type(self):
+ """See base class."""
+ return 'multi ' + self.parser.flag_type()
+
+ def _extra_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = []
+ if hasattr(self.parser, 'enum_values'):
+ for enum_value in self.parser.enum_values: # pytype: disable=attribute-error
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'enum_value', enum_value))
+ return elements
+
+
+class MultiEnumClassFlag(MultiFlag[_ET]): # pytype: disable=not-indexable
+ """A multi_enum_class flag.
+
+ See the __doc__ for MultiFlag for most behaviors of this class. In addition,
+ this class knows how to handle enum.Enum instances as values for this flag
+ type.
+ """
+
+ parser: _argument_parser.EnumClassParser[_ET] # type: ignore[assignment]
+
+ def __init__(
+ self,
+ name: str,
+ default: None | Iterable[_ET] | _ET | Iterable[str] | str,
+ help_string: str,
+ enum_class: type[_ET],
+ case_sensitive: bool = False,
+ **args
+ ):
+ p = _argument_parser.EnumClassParser(
+ enum_class, case_sensitive=case_sensitive)
+ g: _argument_parser.EnumClassListSerializer
+ g = _argument_parser.EnumClassListSerializer(
+ list_sep=',', lowercase=not case_sensitive)
+ super().__init__(p, g, name, default, help_string, **args)
+ # NOTE: parser should be typed EnumClassParser[_ET] but the constructor
+ # restricts the available interface to ArgumentParser[str].
+ self.parser = p
+ # NOTE: serializer should be non-Optional but this isn't inferred.
+ self.serializer = g
+ self.help = (
+ '<%s>: %s;\n repeat this option to specify a list of values' %
+ ('|'.join(p.member_names), help_string or '(no help available)'))
+
+ def _extra_xml_dom_elements(
+ self, doc: minidom.Document
+ ) -> list[minidom.Element]:
+ elements = []
+ for enum_value in self.parser.enum_class.__members__.keys(): # pytype: disable=attribute-error
+ elements.append(_helpers.create_xml_dom_element(
+ doc, 'enum_value', enum_value))
+ return elements
+
+ def _serialize_value_for_xml(self, value):
+ """See base class."""
+ if value is not None:
+ if not self.serializer:
+ raise _exceptions.Error(
+ 'Serializer not present for flag %s' % self.name
+ )
+ value_serialized = self.serializer.serialize(value)
+ else:
+ value_serialized = ''
+ return value_serialized
diff --git a/venv/Lib/site-packages/absl/flags/_flagvalues.py b/venv/Lib/site-packages/absl/flags/_flagvalues.py
new file mode 100644
index 0000000000000000000000000000000000000000..fa0e9f68ec7bac69e1bffc7ceb1bc664ddf2eee5
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_flagvalues.py
@@ -0,0 +1,1552 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Defines the FlagValues class - registry of 'Flag' objects.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+from collections.abc import Callable, Iterable, Iterator, Sequence
+import copy
+from importlib import abc
+import logging
+import os
+import sys
+from typing import Any, Generic, TextIO, TypeVar
+from xml.dom import minidom
+
+from absl.flags import _exceptions
+from absl.flags import _flag
+from absl.flags import _helpers
+from absl.flags import _validators_classes
+from absl.flags._flag import Flag
+
+# Add flagvalues module to disclaimed module ids.
+_helpers.disclaim_module_ids.add(id(sys.modules[__name__]))
+
+_T = TypeVar('_T')
+_T_co = TypeVar('_T_co', covariant=True) # pytype: disable=not-supported-yet
+
+
+class ReloadDetector(abc.MetaPathFinder):
+ """Helper class for detecting reloads."""
+
+ def __init__(self):
+ self.reloading_modules = set()
+
+ def find_spec(self, fullname, path, target=None):
+ if fullname in sys.modules: # Indicates a reload.
+ self.reloading_modules.add(fullname)
+ return None
+
+
+reload_detector = ReloadDetector()
+
+# Register the hook by inserting it right before the last path finder.
+# This should play nicely with lazy imports.
+sys.meta_path.insert(-1, reload_detector)
+
+
+class FlagValues:
+ """Registry of :class:`~absl.flags.Flag` objects.
+
+ A :class:`FlagValues` can then scan command line arguments, passing flag
+ arguments through to the 'Flag' objects that it owns. It also
+ provides easy access to the flag values. Typically only one
+ :class:`FlagValues` object is needed by an application:
+ :const:`FLAGS`.
+
+ This class is heavily overloaded:
+
+ :class:`Flag` objects are registered via ``__setitem__``::
+
+ FLAGS['longname'] = x # register a new flag
+
+ The ``.value`` attribute of the registered :class:`~absl.flags.Flag` objects
+ can be accessed as attributes of this :class:`FlagValues` object, through
+ ``__getattr__``. Both the long and short name of the original
+ :class:`~absl.flags.Flag` objects can be used to access its value::
+
+ FLAGS.longname # parsed flag value
+ FLAGS.x # parsed flag value (short name)
+
+ Command line arguments are scanned and passed to the registered
+ :class:`~absl.flags.Flag` objects through the ``__call__`` method. Unparsed
+ arguments, including ``argv[0]`` (e.g. the program name) are returned::
+
+ argv = FLAGS(sys.argv) # scan command line arguments
+
+ The original registered :class:`~absl.flags.Flag` objects can be retrieved
+ through the use of the dictionary-like operator, ``__getitem__``::
+
+ x = FLAGS['longname'] # access the registered Flag object
+
+ The ``str()`` operator of a :class:`absl.flags.FlagValues` object provides
+ help for all of the registered :class:`~absl.flags.Flag` objects.
+ """
+
+ _HAS_DYNAMIC_ATTRIBUTES = True
+
+ # A note on collections.abc.Mapping:
+ # FlagValues defines __getitem__, __iter__, and __len__. It makes perfect
+ # sense to let it be a collections.abc.Mapping class. However, we are not
+ # able to do so. The mixin methods, e.g. keys, values, are not uncommon flag
+ # names. Those flag values would not be accessible via the FLAGS.xxx form.
+
+ __dict__: dict[str, Any]
+
+ def __init__(self):
+ # Since everything in this class is so heavily overloaded, the only
+ # way of defining and using fields is to access __dict__ directly.
+
+ # Dictionary: flag name (string) -> Flag object.
+ self.__dict__['__flags'] = {}
+
+ # Set: name of hidden flag (string).
+ # Holds flags that should not be directly accessible from Python.
+ self.__dict__['__hiddenflags'] = set()
+
+ # Dictionary: module name (string) -> list of Flag objects that are defined
+ # by that module.
+ self.__dict__['__flags_by_module'] = {}
+ # Dictionary: module id (int) -> list of Flag objects that are defined by
+ # that module.
+ self.__dict__['__flags_by_module_id'] = {}
+ # Dictionary: module name (string) -> list of Flag objects that are
+ # key for that module.
+ self.__dict__['__key_flags_by_module'] = {}
+
+ # Bool: True if flags were parsed.
+ self.__dict__['__flags_parsed'] = False
+
+ # Bool: True if unparse_flags() was called.
+ self.__dict__['__unparse_flags_called'] = False
+
+ # None or Method(name, value) to call from __setattr__ for an unknown flag.
+ self.__dict__['__set_unknown'] = None
+
+ # A set of banned flag names. This is to prevent users from accidentally
+ # defining a flag that has the same name as a method on this class.
+ # Users can still allow defining the flag by passing
+ # allow_using_method_names=True in DEFINE_xxx functions.
+ self.__dict__['__banned_flag_names'] = frozenset(dir(FlagValues))
+
+ # Bool: Whether to use GNU style scanning.
+ self.__dict__['__use_gnu_getopt'] = True
+
+ # Bool: Whether use_gnu_getopt has been explicitly set by the user.
+ self.__dict__['__use_gnu_getopt_explicitly_set'] = False
+
+ # Function: Takes a flag name as parameter, returns a tuple
+ # (is_retired, type_is_bool).
+ self.__dict__['__is_retired_flag_func'] = None
+
+ def set_gnu_getopt(self, gnu_getopt: bool = True) -> None:
+ """Sets whether or not to use GNU style scanning.
+
+ GNU style allows mixing of flag and non-flag arguments. See
+ http://docs.python.org/library/getopt.html#getopt.gnu_getopt
+
+ Args:
+ gnu_getopt: bool, whether or not to use GNU style scanning.
+ """
+ self.__dict__['__use_gnu_getopt'] = gnu_getopt
+ self.__dict__['__use_gnu_getopt_explicitly_set'] = True
+
+ def is_gnu_getopt(self) -> bool:
+ return self.__dict__['__use_gnu_getopt']
+
+ def _flags(self) -> dict[str, Flag]:
+ return self.__dict__['__flags']
+
+ def flags_by_module_dict(self) -> dict[str, list[Flag]]:
+ """Returns the dictionary of module_name -> list of defined flags.
+
+ Returns:
+ A dictionary. Its keys are module names (strings). Its values
+ are lists of Flag objects.
+ """
+ return self.__dict__['__flags_by_module']
+
+ def flags_by_module_id_dict(self) -> dict[int, list[Flag]]:
+ """Returns the dictionary of module_id -> list of defined flags.
+
+ Returns:
+ A dictionary. Its keys are module IDs (ints). Its values
+ are lists of Flag objects.
+ """
+ return self.__dict__['__flags_by_module_id']
+
+ def key_flags_by_module_dict(self) -> dict[str, list[Flag]]:
+ """Returns the dictionary of module_name -> list of key flags.
+
+ Returns:
+ A dictionary. Its keys are module names (strings). Its values
+ are lists of Flag objects.
+ """
+ return self.__dict__['__key_flags_by_module']
+
+ def register_flag_by_module(self, module_name: str, flag: Flag) -> None:
+ """Records the module that defines a specific flag.
+
+ We keep track of which flag is defined by which module so that we
+ can later sort the flags by module.
+
+ Args:
+ module_name: str, the name of a Python module.
+ flag: Flag, the Flag instance that is key to the module.
+ """
+ flags_by_module = self.flags_by_module_dict()
+ flags_by_module.setdefault(module_name, []).append(flag)
+
+ def register_flag_by_module_id(self, module_id: int, flag: Flag) -> None:
+ """Records the module that defines a specific flag.
+
+ Args:
+ module_id: int, the ID of the Python module.
+ flag: Flag, the Flag instance that is key to the module.
+ """
+ flags_by_module_id = self.flags_by_module_id_dict()
+ flags_by_module_id.setdefault(module_id, []).append(flag)
+
+ def register_key_flag_for_module(self, module_name: str, flag: Flag) -> None:
+ """Specifies that a flag is a key flag for a module.
+
+ Args:
+ module_name: str, the name of a Python module.
+ flag: Flag, the Flag instance that is key to the module.
+ """
+ key_flags_by_module = self.key_flags_by_module_dict()
+ # The list of key flags for the module named module_name.
+ key_flags = key_flags_by_module.setdefault(module_name, [])
+ # Add flag, but avoid duplicates.
+ if flag not in key_flags:
+ key_flags.append(flag)
+
+ def _flag_is_registered(self, flag_obj: Flag) -> bool:
+ """Checks whether a Flag object is registered under long name or short name.
+
+ Args:
+ flag_obj: Flag, the Flag instance to check for.
+
+ Returns:
+ bool, True iff flag_obj is registered under long name or short name.
+ """
+ flag_dict = self._flags()
+ # Check whether flag_obj is registered under its long name.
+ name = flag_obj.name
+ if name in flag_dict and flag_dict[name] == flag_obj:
+ return True
+ # Check whether flag_obj is registered under its short name.
+ short_name = flag_obj.short_name
+ if (
+ short_name is not None
+ and short_name in flag_dict
+ and flag_dict[short_name] == flag_obj
+ ):
+ return True
+ return False
+
+ def _cleanup_unregistered_flag_from_module_dicts(
+ self, flag_obj: Flag
+ ) -> None:
+ """Cleans up unregistered flags from all module -> [flags] dictionaries.
+
+ If flag_obj is registered under either its long name or short name, it
+ won't be removed from the dictionaries.
+
+ Args:
+ flag_obj: Flag, the Flag instance to clean up for.
+ """
+ if self._flag_is_registered(flag_obj):
+ return
+ # Materialize dict values to list to avoid concurrent modification.
+ for flags_in_module in [
+ *self.flags_by_module_dict().values(),
+ *self.flags_by_module_id_dict().values(),
+ *self.key_flags_by_module_dict().values(),
+ ]:
+ # While (as opposed to if) takes care of multiple occurrences of a
+ # flag in the list for the same module.
+ while flag_obj in flags_in_module:
+ flags_in_module.remove(flag_obj)
+
+ def get_flags_for_module(self, module: str | Any) -> list[Flag]:
+ """Returns the list of flags defined by a module.
+
+ Args:
+ module: module|str, the module to get flags from.
+
+ Returns:
+ [Flag], a new list of Flag instances. Caller may update this list as
+ desired: none of those changes will affect the internals of this
+ FlagValue instance.
+ """
+ if not isinstance(module, str):
+ module = module.__name__
+ if module == '__main__':
+ module = sys.argv[0]
+
+ return list(self.flags_by_module_dict().get(module, []))
+
+ def get_key_flags_for_module(self, module: str | Any) -> list[Flag]:
+ """Returns the list of key flags for a module.
+
+ Args:
+ module: module|str, the module to get key flags from.
+
+ Returns:
+ [Flag], a new list of Flag instances. Caller may update this list as
+ desired: none of those changes will affect the internals of this
+ FlagValue instance.
+ """
+ if not isinstance(module, str):
+ module = module.__name__
+ if module == '__main__':
+ module = sys.argv[0]
+
+ # Any flag is a key flag for the module that defined it. NOTE:
+ # key_flags is a fresh list: we can update it without affecting the
+ # internals of this FlagValues object.
+ key_flags = self.get_flags_for_module(module)
+
+ # Take into account flags explicitly declared as key for a module.
+ for flag in self.key_flags_by_module_dict().get(module, []):
+ if flag not in key_flags:
+ key_flags.append(flag)
+ return key_flags
+
+ # TODO(yileiyang): Restrict default to Optional[str].
+ def find_module_defining_flag(
+ self, flagname: str, default: _T | None = None
+ ) -> str | _T | None:
+ """Return the name of the module defining this flag, or default.
+
+ Args:
+ flagname: str, name of the flag to lookup.
+ default: Value to return if flagname is not defined. Defaults to None.
+
+ Returns:
+ The name of the module which registered the flag with this name.
+ If no such module exists (i.e. no flag with this name exists),
+ we return default.
+ """
+ registered_flag = self._flags().get(flagname)
+ if registered_flag is None:
+ return default
+ for module, flags in self.flags_by_module_dict().items():
+ for flag in flags:
+ # It must compare the flag with the one in _flags. This is because a
+ # flag might be overridden only for its long name (or short name),
+ # and only its short name (or long name) is considered registered.
+ if (
+ flag.name == registered_flag.name
+ and flag.short_name == registered_flag.short_name
+ ):
+ return module
+ return default
+
+ # TODO(yileiyang): Restrict default to Optional[str].
+ def find_module_id_defining_flag(
+ self, flagname: str, default: _T | None = None
+ ) -> int | _T | None:
+ """Return the ID of the module defining this flag, or default.
+
+ Args:
+ flagname: str, name of the flag to lookup.
+ default: Value to return if flagname is not defined. Defaults to None.
+
+ Returns:
+ The ID of the module which registered the flag with this name.
+ If no such module exists (i.e. no flag with this name exists),
+ we return default.
+ """
+ registered_flag = self._flags().get(flagname)
+ if registered_flag is None:
+ return default
+ for module_id, flags in self.flags_by_module_id_dict().items():
+ for flag in flags:
+ # It must compare the flag with the one in _flags. This is because a
+ # flag might be overridden only for its long name (or short name),
+ # and only its short name (or long name) is considered registered.
+ if (
+ flag.name == registered_flag.name
+ and flag.short_name == registered_flag.short_name
+ ):
+ return module_id
+ return default
+
+ def _register_unknown_flag_setter(
+ self, setter: Callable[[str, Any], None]
+ ) -> None:
+ """Allow set default values for undefined flags.
+
+ Args:
+ setter: Method(name, value) to call to __setattr__ an unknown flag. Must
+ raise NameError or ValueError for invalid name/value.
+ """
+ self.__dict__['__set_unknown'] = setter
+
+ def _set_unknown_flag(self, name: str, value: _T) -> _T:
+ """Returns value if setting flag |name| to |value| returned True.
+
+ Args:
+ name: str, name of the flag to set.
+ value: Value to set.
+
+ Returns:
+ Flag value on successful call.
+
+ Raises:
+ UnrecognizedFlagError
+ IllegalFlagValueError
+ """
+ setter = self.__dict__['__set_unknown']
+ if setter:
+ try:
+ setter(name, value)
+ return value
+ except (TypeError, ValueError): # Flag value is not valid.
+ raise _exceptions.IllegalFlagValueError(
+ f'"{value}" is not valid for --{name}'
+ )
+ except NameError: # Flag name is not valid.
+ pass
+ raise _exceptions.UnrecognizedFlagError(name, value)
+
+ def append_flag_values(self, flag_values: 'FlagValues') -> None:
+ """Appends flags registered in another FlagValues instance.
+
+ Args:
+ flag_values: FlagValues, the FlagValues instance from which to copy flags.
+ """
+ for flag_name, flag in flag_values._flags().items(): # pylint: disable=protected-access
+ # Each flags with short_name appears here twice (once under its
+ # normal name, and again with its short name). To prevent
+ # problems (DuplicateFlagError) with double flag registration, we
+ # perform a check to make sure that the entry we're looking at is
+ # for its normal name.
+ if flag_name == flag.name:
+ try:
+ self[flag_name] = flag
+ except _exceptions.DuplicateFlagError:
+ raise _exceptions.DuplicateFlagError.from_flag(
+ flag_name, self, other_flag_values=flag_values
+ )
+
+ def remove_flag_values(
+ self, flag_values: 'FlagValues | Iterable[str]'
+ ) -> None:
+ """Remove flags that were previously appended from another FlagValues.
+
+ Args:
+ flag_values: FlagValues, the FlagValues instance containing flags to
+ remove.
+ """
+ for flag_name in flag_values:
+ self.__delattr__(flag_name)
+
+ def __setitem__(self, name: str, flag: Flag) -> None:
+ """Registers a new flag variable."""
+ fl = self._flags()
+ if not isinstance(flag, _flag.Flag):
+ raise _exceptions.IllegalFlagValueError(
+ f'Expect Flag instances, found type {type(flag)}. '
+ "Maybe you didn't mean to use FlagValue.__setitem__?"
+ )
+ if not isinstance(name, str):
+ raise _exceptions.Error('Flag name must be a string')
+ if not name:
+ raise _exceptions.Error('Flag name cannot be empty')
+ if ' ' in name:
+ raise _exceptions.Error('Flag name cannot contain a space')
+ self._check_method_name_conflicts(name, flag)
+ if name in fl and not flag.allow_override and not fl[name].allow_override:
+ module, module_name = _helpers.get_calling_module_object_and_name()
+ if self.find_module_defining_flag(name) == module_name and (
+ id(module) != self.find_module_id_defining_flag(name)
+ or module_name in reload_detector.reloading_modules
+ ):
+ # If the flag has already been defined by a module with the same name,
+ # but a different ID, we can stop here because it indicates that the
+ # module is simply being imported a subsequent time.
+ # In case the module is being reloaded (using `importlib.reload`), it'll
+ # have the same ID, so we detect it using reload_detector.
+ return
+ raise _exceptions.DuplicateFlagError.from_flag(name, self)
+ # If a new flag overrides an old one, we need to cleanup the old flag's
+ # modules if it's not registered.
+ flags_to_cleanup = set()
+ short_name: str | None = flag.short_name
+ if short_name is not None:
+ if (
+ short_name in fl
+ and not flag.allow_override
+ and not fl[short_name].allow_override
+ ):
+ raise _exceptions.DuplicateFlagError.from_flag(short_name, self)
+ if short_name in fl and fl[short_name] != flag:
+ flags_to_cleanup.add(fl[short_name])
+ fl[short_name] = flag
+ if (
+ name not in fl # new flag
+ or fl[name].using_default_value
+ or not flag.using_default_value
+ ):
+ if name in fl and fl[name] != flag:
+ flags_to_cleanup.add(fl[name])
+ fl[name] = flag
+ for f in flags_to_cleanup:
+ self._cleanup_unregistered_flag_from_module_dicts(f)
+
+ def __dir__(self) -> list[str]:
+ """Returns list of names of all defined flags.
+
+ Useful for TAB-completion in ipython.
+
+ Returns:
+ [str], a list of names of all defined flags.
+ """
+ return sorted(self._flags())
+
+ def __getitem__(self, name: str) -> Flag:
+ """Returns the Flag object for the flag --name."""
+ return self._flags()[name]
+
+ def _hide_flag(self, name):
+ """Marks the flag --name as hidden."""
+ self.__dict__['__hiddenflags'].add(name)
+
+ def __getattr__(self, name: str) -> Any:
+ """Retrieves the 'value' attribute of the flag --name."""
+ flag_entry = self._flags().get(name)
+ if flag_entry is None:
+ raise AttributeError(name)
+ if name in self.__dict__['__hiddenflags']:
+ raise AttributeError(name)
+
+ if self.__dict__['__flags_parsed'] or flag_entry.present:
+ return flag_entry.value
+ else:
+ raise _exceptions.UnparsedFlagAccessError(
+ 'Trying to access flag --%s before flags were parsed.' % name
+ )
+
+ def __setattr__(self, name: str, value: _T) -> _T:
+ """Sets the 'value' attribute of the flag --name."""
+ self._set_attributes(**{name: value})
+ return value
+
+ def _set_attributes(self, **attributes: Any) -> None:
+ """Sets multiple flag values together, triggers validators afterwards."""
+ fl = self._flags()
+ known_flag_vals = {}
+ known_flag_used_defaults = {}
+ try:
+ for name, value in attributes.items():
+ if name in self.__dict__['__hiddenflags']:
+ raise AttributeError(name)
+ flag_entry = fl.get(name)
+ if flag_entry is not None:
+ orig = flag_entry.value
+ flag_entry.value = value
+ known_flag_vals[name] = orig
+ else:
+ self._set_unknown_flag(name, value)
+ for name in known_flag_vals:
+ self._assert_validators(fl[name].validators)
+ known_flag_used_defaults[name] = fl[name].using_default_value
+ fl[name].using_default_value = False
+ except:
+ for name, orig in known_flag_vals.items():
+ fl[name].value = orig
+ for name, orig in known_flag_used_defaults.items():
+ fl[name].using_default_value = orig
+ # NOTE: We do not attempt to undo unknown flag side effects because we
+ # cannot reliably undo the user-configured behavior.
+ raise
+
+ def validate_all_flags(self) -> None:
+ """Verifies whether all flags pass validation.
+
+ Raises:
+ AttributeError: Raised if validators work with a non-existing flag.
+ IllegalFlagValueError: Raised if validation fails for at least one
+ validator.
+ """
+ all_validators = set()
+ for flag in self._flags().values():
+ all_validators.update(flag.validators)
+ self._assert_validators(all_validators)
+
+ def _assert_validators(
+ self, validators: Iterable[_validators_classes.Validator]
+ ) -> None:
+ """Asserts if all validators in the list are satisfied.
+
+ It asserts validators in the order they were created.
+
+ Args:
+ validators: Iterable(validators.Validator), validators to be verified.
+
+ Raises:
+ AttributeError: Raised if validators work with a non-existing flag.
+ IllegalFlagValueError: Raised if validation fails for at least one
+ validator.
+ """
+ messages = []
+ bad_flags: set[str] = set()
+ for validator in sorted(
+ validators, key=lambda validator: validator.insertion_index
+ ):
+ try:
+ if isinstance(validator, _validators_classes.SingleFlagValidator):
+ if validator.flag_name in bad_flags:
+ continue
+ elif isinstance(validator, _validators_classes.MultiFlagsValidator):
+ if bad_flags & set(validator.flag_names):
+ continue
+ validator.verify(self)
+ except _exceptions.ValidationError as e:
+ if isinstance(validator, _validators_classes.SingleFlagValidator):
+ bad_flags.add(validator.flag_name)
+ elif isinstance(validator, _validators_classes.MultiFlagsValidator):
+ bad_flags.update(set(validator.flag_names))
+ message = validator.print_flags_with_values(self)
+ messages.append('%s: %s' % (message, str(e)))
+ if messages:
+ raise _exceptions.IllegalFlagValueError('\n'.join(messages))
+
+ def __delattr__(self, flag_name: str) -> None:
+ """Deletes a previously-defined flag from a flag object.
+
+ This method makes sure we can delete a flag by using
+
+ del FLAGS.
+
+ E.g.,
+
+ flags.DEFINE_integer('foo', 1, 'Integer flag.')
+ del flags.FLAGS.foo
+
+ If a flag is also registered by its the other name (long name or short
+ name), the other name won't be deleted.
+
+ Args:
+ flag_name: str, the name of the flag to be deleted.
+
+ Raises:
+ AttributeError: Raised when there is no registered flag named flag_name.
+ """
+ fl = self._flags()
+ flag_entry = fl.get(flag_name)
+ if flag_entry is None:
+ raise AttributeError(flag_name)
+ del fl[flag_name]
+
+ self._cleanup_unregistered_flag_from_module_dicts(flag_entry)
+
+ def set_default(self, name: str, value: Any) -> None:
+ """Changes the default value of the named flag object.
+
+ The flag's current value is also updated if the flag is currently using
+ the default value, i.e. not specified in the command line, and not set
+ by FLAGS.name = value.
+
+ Args:
+ name: str, the name of the flag to modify.
+ value: The new default value.
+
+ Raises:
+ UnrecognizedFlagError: Raised when there is no registered flag named name.
+ IllegalFlagValueError: Raised when value is not valid.
+ """
+ fl = self._flags()
+ flag_entry = fl.get(name)
+ if flag_entry is None:
+ self._set_unknown_flag(name, value)
+ return
+ flag_entry._set_default(value) # pylint: disable=protected-access
+ self._assert_validators(flag_entry.validators)
+
+ def __contains__(self, name: str) -> bool:
+ """Returns True if name is a value (flag) in the dict."""
+ return name in self._flags()
+
+ def __len__(self) -> int:
+ return len(self.__dict__['__flags'])
+
+ def __iter__(self) -> Iterator[str]:
+ return iter(self._flags())
+
+ def __call__(
+ self, argv: Sequence[str], known_only: bool = False
+ ) -> list[str]:
+ """Parses flags from argv; stores parsed flags into this FlagValues object.
+
+ All unparsed arguments are returned.
+
+ Args:
+ argv: a tuple/list of strings.
+ known_only: bool, if True, parse and remove known flags; return the rest
+ untouched. Unknown flags specified by --undefok are not returned.
+
+ Returns:
+ The list of arguments not parsed as options, including argv[0].
+
+ Raises:
+ Error: Raised on any parsing error.
+ TypeError: Raised on passing wrong type of arguments.
+ ValueError: Raised on flag value parsing error.
+ """
+ if isinstance(argv, (str, bytes)):
+ raise TypeError(
+ 'argv should be a tuple/list of strings, not bytes or string.'
+ )
+ if not argv:
+ raise ValueError(
+ 'argv cannot be an empty list, and must contain the program name as '
+ 'the first element.'
+ )
+
+ # This pre parses the argv list for --flagfile=<> options.
+ program_name = argv[0]
+ args = self.read_flags_from_files(argv[1:], force_gnu=False)
+
+ # Parse the arguments.
+ unknown_flags, unparsed_args = self._parse_args(args, known_only)
+
+ # Handle unknown flags by raising UnrecognizedFlagError.
+ # Note some users depend on us raising this particular error.
+ for name, value in unknown_flags:
+ suggestions = _helpers.get_flag_suggestions(name, list(self))
+ raise _exceptions.UnrecognizedFlagError(
+ name, value, suggestions=suggestions
+ )
+
+ self.mark_as_parsed()
+ self.validate_all_flags()
+ return [program_name] + unparsed_args
+
+ def __getstate__(self) -> Any:
+ raise TypeError("can't pickle FlagValues")
+
+ def __copy__(self) -> Any:
+ raise TypeError(
+ 'FlagValues does not support shallow copies. '
+ 'Use absl.testing.flagsaver or copy.deepcopy instead.'
+ )
+
+ def __deepcopy__(self, memo) -> Any:
+ result = object.__new__(type(self))
+ result.__dict__.update(copy.deepcopy(self.__dict__, memo))
+ return result
+
+ def _set_is_retired_flag_func(self, is_retired_flag_func):
+ """Sets a function for checking retired flags.
+
+ Do not use it. This is a private absl API used to check retired flags
+ registered by the absl C++ flags library.
+
+ Args:
+ is_retired_flag_func: Callable(str) -> (bool, bool), a function takes flag
+ name as parameter, returns a tuple (is_retired, type_is_bool).
+ """
+ self.__dict__['__is_retired_flag_func'] = is_retired_flag_func
+
+ def _parse_args(
+ self, args: list[str], known_only: bool
+ ) -> tuple[list[tuple[str, Any]], list[str]]:
+ """Helper function to do the main argument parsing.
+
+ This function goes through args and does the bulk of the flag parsing.
+ It will find the corresponding flag in our flag dictionary, and call its
+ .parse() method on the flag value.
+
+ Args:
+ args: [str], a list of strings with the arguments to parse.
+ known_only: bool, if True, parse and remove known flags; return the rest
+ untouched. Unknown flags specified by --undefok are not returned.
+
+ Returns:
+ A tuple with the following:
+ unknown_flags: List of (flag name, arg) for flags we don't know about.
+ unparsed_args: List of arguments we did not parse.
+
+ Raises:
+ Error: Raised on any parsing error.
+ ValueError: Raised on flag value parsing error.
+ """
+ unparsed_names_and_args: list[tuple[str | None, str]] = []
+ undefok: set[str] = set()
+ retired_flag_func = self.__dict__['__is_retired_flag_func']
+
+ flag_dict = self._flags()
+ args_it = iter(args)
+ del args
+ for arg in args_it:
+ value = None
+
+ def get_value() -> str:
+ try:
+ return next(args_it) if value is None else value # pylint: disable=cell-var-from-loop
+ except StopIteration:
+ raise _exceptions.Error('Missing value for flag ' + arg) from None # pylint: disable=cell-var-from-loop
+
+ if not arg.startswith('-'):
+ # A non-argument: default is break, GNU is skip.
+ unparsed_names_and_args.append((None, arg))
+ if self.is_gnu_getopt():
+ continue
+ else:
+ break
+
+ if arg == '--':
+ if known_only:
+ unparsed_names_and_args.append((None, arg))
+ break
+
+ # At this point, arg must start with '-'.
+ if arg.startswith('--'):
+ arg_without_dashes = arg[2:]
+ else:
+ arg_without_dashes = arg[1:]
+
+ if '=' in arg_without_dashes:
+ name, value = arg_without_dashes.split('=', 1)
+ else:
+ name, value = arg_without_dashes, None
+
+ if not name:
+ # The argument is all dashes (including one dash).
+ unparsed_names_and_args.append((None, arg))
+ if self.is_gnu_getopt():
+ continue
+ else:
+ break
+
+ # --undefok is a special case.
+ if name == 'undefok':
+ value = get_value()
+ undefok.update(v.strip() for v in value.split(','))
+ undefok.update('no' + v.strip() for v in value.split(','))
+ continue
+
+ flag = flag_dict.get(name)
+ if flag is not None:
+ if flag.boolean and value is None:
+ value = 'true'
+ else:
+ value = get_value()
+ elif name.startswith('no') and len(name) > 2:
+ # Boolean flags can take the form of --noflag, with no value.
+ noflag = flag_dict.get(name[2:])
+ if noflag is not None and noflag.boolean:
+ if value is not None:
+ raise ValueError(arg + ' does not take an argument')
+ flag = noflag
+ value = 'false'
+
+ if retired_flag_func and flag is None:
+ is_retired, is_bool = retired_flag_func(name)
+
+ # If we didn't recognize that flag, but it starts with
+ # "no" then maybe it was a boolean flag specified in the
+ # --nofoo form.
+ if not is_retired and name.startswith('no'):
+ is_retired, is_bool = retired_flag_func(name[2:])
+ is_retired = is_retired and is_bool
+
+ if is_retired:
+ if not is_bool and value is None:
+ # This happens when a non-bool retired flag is specified
+ # in format of "--flag value".
+ get_value()
+ logging.error(
+ 'Flag "%s" is retired and should no longer be specified. See '
+ 'https://abseil.io/tips/90.',
+ name,
+ )
+ continue
+
+ if flag is not None:
+ # LINT.IfChange
+ flag.parse(value)
+ flag.using_default_value = False
+ # LINT.ThenChange(../testing/flagsaver.py:flag_override_parsing)
+ else:
+ unparsed_names_and_args.append((name, arg))
+
+ unknown_flags = []
+ unparsed_args = []
+ for arg_name, arg in unparsed_names_and_args:
+ if arg_name is None:
+ # Positional arguments.
+ unparsed_args.append(arg)
+ elif arg_name in undefok:
+ # Remove undefok flags.
+ continue
+ else:
+ # This is an unknown flag.
+ if known_only:
+ unparsed_args.append(arg)
+ else:
+ unknown_flags.append((arg_name, arg))
+
+ unparsed_args.extend(list(args_it))
+ return unknown_flags, unparsed_args
+
+ def is_parsed(self) -> bool:
+ """Returns whether flags were parsed."""
+ return self.__dict__['__flags_parsed']
+
+ def mark_as_parsed(self) -> None:
+ """Explicitly marks flags as parsed.
+
+ Use this when the caller knows that this FlagValues has been parsed as if
+ a ``__call__()`` invocation has happened. This is only a public method for
+ use by things like appcommands which do additional command like parsing.
+ """
+ self.__dict__['__flags_parsed'] = True
+
+ def unparse_flags(self) -> None:
+ """Unparses all flags to the point before any FLAGS(argv) was called."""
+ for f in self._flags().values():
+ f.unparse()
+ # We log this message before marking flags as unparsed to avoid a
+ # problem when the logging library causes flags access.
+ logging.info('unparse_flags() called; flags access will now raise errors.')
+ self.__dict__['__flags_parsed'] = False
+ self.__dict__['__unparse_flags_called'] = True
+
+ def flag_values_dict(self) -> dict[str, Any]:
+ """Returns a dictionary that maps flag names to flag values."""
+ return {name: flag.value for name, flag in list(self._flags().items())}
+
+ def __str__(self):
+ """Returns a help string for all known flags."""
+ return self.get_help()
+
+ def get_help(
+ self, prefix: str = '', include_special_flags: bool = True
+ ) -> str:
+ """Returns a help string for all known flags.
+
+ Args:
+ prefix: str, per-line output prefix.
+ include_special_flags: bool, whether to include description of
+ SPECIAL_FLAGS, i.e. --flagfile and --undefok.
+
+ Returns:
+ str, formatted help message.
+ """
+ flags_by_module = self.flags_by_module_dict()
+ if flags_by_module:
+ modules = sorted(flags_by_module)
+ # Print the help for the main module first, if possible.
+ main_module = sys.argv[0]
+ if main_module in modules:
+ modules.remove(main_module)
+ modules = [main_module] + modules
+ return self._get_help_for_modules(modules, prefix, include_special_flags)
+ else:
+ output_lines: list[str] = []
+ # Just print one long list of flags.
+ values = list(self._flags().values())
+ if include_special_flags:
+ values.extend(_helpers.SPECIAL_FLAGS._flags().values()) # pylint: disable=protected-access
+ self._render_flag_list(values, output_lines, prefix)
+ return '\n'.join(output_lines)
+
+ def _get_help_for_modules(self, modules, prefix, include_special_flags):
+ """Returns the help string for a list of modules.
+
+ Private to absl.flags package.
+
+ Args:
+ modules: List[str], a list of modules to get the help string for.
+ prefix: str, a string that is prepended to each generated help line.
+ include_special_flags: bool, whether to include description of
+ SPECIAL_FLAGS, i.e. --flagfile and --undefok.
+ """
+ output_lines = []
+ for module in modules:
+ self._render_our_module_flags(module, output_lines, prefix)
+ if include_special_flags:
+ self._render_module_flags(
+ 'absl.flags',
+ _helpers.SPECIAL_FLAGS._flags().values(), # pylint: disable=protected-access # pytype: disable=attribute-error
+ output_lines,
+ prefix,
+ )
+ return '\n'.join(output_lines)
+
+ def _render_module_flags(self, module, flags, output_lines, prefix=''):
+ """Returns a help string for a given module."""
+ if not isinstance(module, str):
+ module = module.__name__
+ output_lines.append('\n%s%s:' % (prefix, module))
+ self._render_flag_list(flags, output_lines, prefix + ' ')
+
+ def _render_our_module_flags(self, module, output_lines, prefix=''):
+ """Returns a help string for a given module."""
+ flags = self.get_flags_for_module(module)
+ if flags:
+ self._render_module_flags(module, flags, output_lines, prefix)
+
+ def _render_our_module_key_flags(self, module, output_lines, prefix=''):
+ """Returns a help string for the key flags of a given module.
+
+ Args:
+ module: module|str, the module to render key flags for.
+ output_lines: [str], a list of strings. The generated help message lines
+ will be appended to this list.
+ prefix: str, a string that is prepended to each generated help line.
+ """
+ key_flags = self.get_key_flags_for_module(module)
+ if key_flags:
+ self._render_module_flags(module, key_flags, output_lines, prefix)
+
+ def module_help(self, module: Any) -> str:
+ """Describes the key flags of a module.
+
+ Args:
+ module: module|str, the module to describe the key flags for.
+
+ Returns:
+ str, describing the key flags of a module.
+ """
+ helplist: list[str] = []
+ self._render_our_module_key_flags(module, helplist)
+ return '\n'.join(helplist)
+
+ def main_module_help(self) -> str:
+ """Describes the key flags of the main module.
+
+ Returns:
+ str, describing the key flags of the main module.
+ """
+ return self.module_help(sys.argv[0])
+
+ def _render_flag_list(self, flaglist, output_lines, prefix=' '):
+ fl = self._flags()
+ special_fl = _helpers.SPECIAL_FLAGS._flags() # pylint: disable=protected-access # pytype: disable=attribute-error
+ flaglist = [(flag.name, flag) for flag in flaglist]
+ flaglist.sort()
+ flagset = {}
+ for name, flag in flaglist:
+ # It's possible this flag got deleted or overridden since being
+ # registered in the per-module flaglist. Check now against the
+ # canonical source of current flag information, the _flags.
+ if fl.get(name, None) != flag and special_fl.get(name, None) != flag:
+ # a different flag is using this name now
+ continue
+ # only print help once
+ if flag in flagset:
+ continue
+ flagset[flag] = 1
+ flaghelp = ''
+ if flag.short_name:
+ flaghelp += '-%s,' % flag.short_name
+ if flag.boolean:
+ flaghelp += '--[no]%s:' % flag.name
+ else:
+ flaghelp += '--%s:' % flag.name
+ flaghelp += ' '
+ if flag.help:
+ flaghelp += flag.help
+ flaghelp = _helpers.text_wrap(
+ flaghelp, indent=prefix + ' ', firstline_indent=prefix
+ )
+ if flag.default_as_str:
+ flaghelp += '\n'
+ flaghelp += _helpers.text_wrap(
+ '(default: %s)' % flag.default_as_str, indent=prefix + ' '
+ )
+ if flag.parser.syntactic_help:
+ flaghelp += '\n'
+ flaghelp += _helpers.text_wrap(
+ '(%s)' % flag.parser.syntactic_help, indent=prefix + ' '
+ )
+ output_lines.append(flaghelp)
+
+ def get_flag_value(self, name: str, default: Any) -> Any: # pylint: disable=invalid-name
+ """Returns the value of a flag (if not None) or a default value.
+
+ Args:
+ name: str, the name of a flag.
+ default: Default value to use if the flag value is None.
+
+ Returns:
+ Requested flag value or default.
+ """
+
+ value = self.__getattr__(name)
+ if value is not None: # Can't do if not value, b/c value might be '0' or ""
+ return value
+ else:
+ return default
+
+ def _is_flag_file_directive(self, flag_string):
+ """Checks whether flag_string contain a --flagfile= directive."""
+ if isinstance(flag_string, str):
+ if flag_string.startswith('--flagfile='):
+ return 1
+ elif flag_string == '--flagfile':
+ return 1
+ elif flag_string.startswith('-flagfile='):
+ return 1
+ elif flag_string == '-flagfile':
+ return 1
+ else:
+ return 0
+ return 0
+
+ def _extract_filename(self, flagfile_str):
+ """Returns filename from a flagfile_str of form -[-]flagfile=filename.
+
+ The cases of --flagfile foo and -flagfile foo shouldn't be hitting
+ this function, as they are dealt with in the level above this
+ function.
+
+ Args:
+ flagfile_str: str, the flagfile string.
+
+ Returns:
+ str, the filename from a flagfile_str of form -[-]flagfile=filename.
+
+ Raises:
+ Error: Raised when illegal --flagfile is provided.
+ """
+ if flagfile_str.startswith('--flagfile='):
+ return os.path.expanduser((flagfile_str[(len('--flagfile=')) :]).strip())
+ elif flagfile_str.startswith('-flagfile='):
+ return os.path.expanduser((flagfile_str[(len('-flagfile=')) :]).strip())
+ else:
+ raise _exceptions.Error('Hit illegal --flagfile type: %s' % flagfile_str)
+
+ def _get_flag_file_lines(self, filename, parsed_file_stack=None):
+ """Returns the useful (!=comments, etc) lines from a file with flags.
+
+ Args:
+ filename: str, the name of the flag file.
+ parsed_file_stack: [str], a list of the names of the files that we have
+ recursively encountered at the current depth. MUTATED BY THIS FUNCTION
+ (but the original value is preserved upon successfully returning from
+ function call).
+
+ Returns:
+ List of strings. See the note below.
+
+ NOTE(springer): This function checks for a nested --flagfile=
+ tag and handles the lower file recursively. It returns a list of
+ all the lines that _could_ contain command flags. This is
+ EVERYTHING except whitespace lines and comments (lines starting
+ with '#' or '//').
+ """
+ # For consistency with the cpp version, ignore empty values.
+ if not filename:
+ return []
+ if parsed_file_stack is None:
+ parsed_file_stack = []
+ # We do a little safety check for reparsing a file we've already encountered
+ # at a previous depth.
+ if filename in parsed_file_stack:
+ sys.stderr.write(
+ 'Warning: Hit circular flagfile dependency. Ignoring flagfile: %s\n'
+ % (filename,)
+ )
+ return []
+ else:
+ parsed_file_stack.append(filename)
+
+ line_list = [] # All line from flagfile.
+ flag_line_list = [] # Subset of lines w/o comments, blanks, flagfile= tags.
+ try:
+ file_obj = open(filename)
+ except OSError as e_msg:
+ raise _exceptions.CantOpenFlagFileError(
+ 'ERROR:: Unable to open flagfile: %s' % e_msg
+ )
+
+ with file_obj:
+ line_list = file_obj.readlines()
+
+ # This is where we check each line in the file we just read.
+ for line in line_list:
+ if line.isspace():
+ pass
+ # Checks for comment (a line that starts with '#').
+ elif line.startswith('#') or line.startswith('//'):
+ pass
+ # Checks for a nested "--flagfile=" flag in the current file.
+ # If we find one, recursively parse down into that file.
+ elif self._is_flag_file_directive(line):
+ sub_filename = self._extract_filename(line)
+ included_flags = self._get_flag_file_lines(
+ sub_filename, parsed_file_stack=parsed_file_stack
+ )
+ flag_line_list.extend(included_flags)
+ else:
+ # Any line that's not a comment or a nested flagfile should get
+ # copied into 2nd position. This leaves earlier arguments
+ # further back in the list, thus giving them higher priority.
+ flag_line_list.append(line.strip())
+
+ parsed_file_stack.pop()
+ return flag_line_list
+
+ def read_flags_from_files(
+ self, argv: Sequence[str], force_gnu: bool = True
+ ) -> list[str]:
+ """Processes command line args, but also allow args to be read from file.
+
+ Args:
+ argv: [str], a list of strings, usually sys.argv[1:], which may contain
+ one or more flagfile directives of the form --flagfile="./filename".
+ Note that the name of the program (sys.argv[0]) should be omitted.
+ force_gnu: bool, if False, --flagfile parsing obeys the
+ FLAGS.is_gnu_getopt() value. If True, ignore the value and always follow
+ gnu_getopt semantics.
+
+ Returns:
+ A new list which has the original list combined with what we read
+ from any flagfile(s).
+
+ Raises:
+ IllegalFlagValueError: Raised when --flagfile is provided with no
+ argument.
+
+ This function is called by FLAGS(argv).
+ It scans the input list for a flag that looks like:
+ --flagfile=. Then it opens , reads all valid key
+ and value pairs and inserts them into the input list in exactly the
+ place where the --flagfile arg is found.
+
+ Note that your application's flags are still defined the usual way
+ using absl.flags DEFINE_flag() type functions.
+
+ Notes (assuming we're getting a commandline of some sort as our input):
+
+ * For duplicate flags, the last one we hit should "win".
+ * Since flags that appear later win, a flagfile's settings can be "weak"
+ if the --flagfile comes at the beginning of the argument sequence,
+ and it can be "strong" if the --flagfile comes at the end.
+ * A further "--flagfile=" CAN be nested in a flagfile.
+ It will be expanded in exactly the spot where it is found.
+ * In a flagfile, a line beginning with # or // is a comment.
+ * Entirely blank lines _should_ be ignored.
+ """
+ rest_of_args = argv
+ new_argv = []
+ while rest_of_args:
+ current_arg = rest_of_args[0]
+ rest_of_args = rest_of_args[1:]
+ if self._is_flag_file_directive(current_arg):
+ # This handles the case of -(-)flagfile foo. In this case the
+ # next arg really is part of this one.
+ if current_arg == '--flagfile' or current_arg == '-flagfile':
+ if not rest_of_args:
+ raise _exceptions.IllegalFlagValueError(
+ '--flagfile with no argument'
+ )
+ flag_filename = os.path.expanduser(rest_of_args[0])
+ rest_of_args = rest_of_args[1:]
+ else:
+ # This handles the case of (-)-flagfile=foo.
+ flag_filename = self._extract_filename(current_arg)
+ new_argv.extend(self._get_flag_file_lines(flag_filename))
+ else:
+ new_argv.append(current_arg)
+ # Stop parsing after '--', like getopt and gnu_getopt.
+ if current_arg == '--':
+ break
+ # Stop parsing after a non-flag, like getopt.
+ if not current_arg.startswith('-'):
+ if not force_gnu and not self.__dict__['__use_gnu_getopt']:
+ break
+ else:
+ if (
+ '=' not in current_arg
+ and rest_of_args
+ and not rest_of_args[0].startswith('-')
+ ):
+ # If this is an occurrence of a legitimate --x y, skip the value
+ # so that it won't be mistaken for a standalone arg.
+ fl = self._flags()
+ name = current_arg.lstrip('-')
+ if name in fl and not fl[name].boolean:
+ current_arg = rest_of_args[0]
+ rest_of_args = rest_of_args[1:]
+ new_argv.append(current_arg)
+
+ if rest_of_args:
+ new_argv.extend(rest_of_args)
+
+ return new_argv
+
+ def flags_into_string(self) -> str:
+ """Returns a string with the flags assignments from this FlagValues object.
+
+ This function ignores flags whose value is None. Each flag
+ assignment is separated by a newline.
+
+ NOTE: MUST mirror the behavior of the C++ CommandlineFlagsIntoString
+ from https://github.com/gflags/gflags.
+
+ Returns:
+ str, the string with the flags assignments from this FlagValues object.
+ The flags are ordered by (module_name, flag_name).
+ """
+ module_flags = sorted(self.flags_by_module_dict().items())
+ s = ''
+ for unused_module_name, flags in module_flags:
+ flags = sorted(flags, key=lambda f: f.name)
+ for flag in flags:
+ if flag.value is not None:
+ s += flag.serialize() + '\n'
+ return s
+
+ def append_flags_into_file(self, filename: str) -> None:
+ """Appends all flags assignments from this FlagInfo object to a file.
+
+ Output will be in the format of a flagfile.
+
+ NOTE: MUST mirror the behavior of the C++ AppendFlagsIntoFile
+ from https://github.com/gflags/gflags.
+
+ Args:
+ filename: str, name of the file.
+ """
+ with open(filename, 'a') as out_file:
+ out_file.write(self.flags_into_string())
+
+ def write_help_in_xml_format(self, outfile: TextIO | None = None) -> None:
+ """Outputs flag documentation in XML format.
+
+ NOTE: We use element names that are consistent with those used by
+ the C++ command-line flag library, from
+ https://github.com/gflags/gflags.
+ We also use a few new elements (e.g., ), but we do not
+ interfere / overlap with existing XML elements used by the C++
+ library. Please maintain this consistency.
+
+ Args:
+ outfile: File object we write to. Default None means sys.stdout.
+ """
+ doc = minidom.Document()
+ all_flag = doc.createElement('AllFlags')
+ doc.appendChild(all_flag)
+
+ all_flag.appendChild(
+ _helpers.create_xml_dom_element(
+ doc, 'program', os.path.basename(sys.argv[0])
+ )
+ )
+
+ usage_doc = sys.modules['__main__'].__doc__
+ if not usage_doc:
+ usage_doc = '\nUSAGE: %s [flags]\n' % sys.argv[0]
+ else:
+ usage_doc = usage_doc.replace('%s', sys.argv[0])
+ all_flag.appendChild(
+ _helpers.create_xml_dom_element(doc, 'usage', usage_doc)
+ )
+
+ # Get list of key flags for the main module.
+ key_flags = self.get_key_flags_for_module(sys.argv[0])
+
+ flags_by_module = self.flags_by_module_dict()
+ # Sort flags by declaring module name and next by flag name.
+ for module_name in sorted(flags_by_module.keys()):
+ flag_list = [(f.name, f) for f in flags_by_module[module_name]]
+ flag_list.sort()
+ for unused_flag_name, flag in flag_list:
+ is_key = flag in key_flags
+ all_flag.appendChild(
+ flag._create_xml_dom_element( # pylint: disable=protected-access
+ doc, module_name, is_key=is_key
+ )
+ )
+
+ outfile = outfile or sys.stdout
+ outfile.write(
+ doc.toprettyxml(indent=' ', encoding='utf-8').decode('utf-8')
+ )
+ outfile.flush()
+
+ def _check_method_name_conflicts(self, name: str, flag: Flag):
+ if flag.allow_using_method_names:
+ return
+ short_name = flag.short_name
+ flag_names = {name} if short_name is None else {name, short_name}
+ for flag_name in flag_names:
+ if flag_name in self.__dict__['__banned_flag_names']:
+ raise _exceptions.FlagNameConflictsWithMethodError(
+ 'Cannot define a flag named "{name}". It conflicts with a method '
+ 'on class "{class_name}". To allow defining it, use '
+ 'allow_using_method_names and access the flag value with '
+ "FLAGS['{name}'].value. FLAGS.{name} returns the method, "
+ 'not the flag value.'.format(
+ name=flag_name, class_name=type(self).__name__
+ )
+ )
+
+
+FLAGS = FlagValues()
+
+
+class FlagHolder(Generic[_T_co]):
+ """Holds a defined flag.
+
+ This facilitates a cleaner api around global state. Instead of::
+
+ flags.DEFINE_integer('foo', ...)
+ flags.DEFINE_integer('bar', ...)
+
+ def method():
+ # prints parsed value of 'bar' flag
+ print(flags.FLAGS.foo)
+ # runtime error due to typo or possibly bad coding style.
+ print(flags.FLAGS.baz)
+
+ it encourages code like::
+
+ _FOO_FLAG = flags.DEFINE_integer('foo', ...)
+ _BAR_FLAG = flags.DEFINE_integer('bar', ...)
+
+ def method():
+ print(_FOO_FLAG.value)
+ print(_BAR_FLAG.value)
+
+ since the name of the flag appears only once in the source code.
+ """
+
+ value: _T_co
+
+ def __init__(
+ self,
+ flag_values: FlagValues,
+ flag: Flag[_T_co],
+ ensure_non_none_value: bool = False,
+ ):
+ """Constructs a FlagHolder instance providing typesafe access to flag.
+
+ Args:
+ flag_values: The container the flag is registered to.
+ flag: The flag object for this flag.
+ ensure_non_none_value: Is the value of the flag allowed to be None.
+ """
+ self._flagvalues = flag_values
+ # We take the entire flag object, but only keep the name. Why?
+ # - We want FlagHolder[T] to be generic container
+ # - flag_values contains all flags, so has no reference to T.
+ # - typecheckers don't like to see a generic class where none of the ctor
+ # arguments refer to the generic type.
+ self._name = flag.name
+ # We intentionally do NOT check if the default value is None.
+ # This allows future use of this for "required flags with None default"
+ self._ensure_non_none_value = ensure_non_none_value
+
+ def __eq__(self, other):
+ raise TypeError(
+ "unsupported operand type(s) for ==: '{0}' and '{1}' "
+ "(did you mean to use '{0}.value' instead?)".format(
+ type(self).__name__, type(other).__name__
+ )
+ )
+
+ def __bool__(self):
+ raise TypeError(
+ "bool() not supported for instances of type '{0}' "
+ "(did you mean to use '{0}.value' instead?)".format(type(self).__name__)
+ )
+
+ __nonzero__ = __bool__
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ @property # type: ignore[no-redef]
+ def value(self) -> _T_co:
+ """Returns the value of the flag.
+
+ If ``_ensure_non_none_value`` is ``True``, then return value is not
+ ``None``.
+
+ Raises:
+ UnparsedFlagAccessError: if flag parsing has not finished.
+ IllegalFlagValueError: if value is None unexpectedly.
+ """
+ val = getattr(self._flagvalues, self._name)
+ if self._ensure_non_none_value and val is None:
+ raise _exceptions.IllegalFlagValueError(
+ 'Unexpected None value for flag %s' % self._name
+ )
+ return val
+
+ @property
+ def default(self) -> _T_co:
+ """Returns the default value of the flag."""
+ return self._flagvalues[self._name].default # type: ignore[return-value]
+
+ @property
+ def present(self) -> bool:
+ """Returns True if the flag was parsed from command-line flags."""
+ return bool(self._flagvalues[self._name].present)
+
+ def serialize(self) -> str:
+ """Returns a serialized representation of the flag."""
+ return self._flagvalues[self._name].serialize()
+
+
+def resolve_flag_ref(
+ flag_ref: str | FlagHolder, flag_values: FlagValues
+) -> tuple[str, FlagValues]:
+ """Helper to validate and resolve a flag reference argument."""
+ if isinstance(flag_ref, FlagHolder):
+ new_flag_values = flag_ref._flagvalues # pylint: disable=protected-access
+ if flag_values != FLAGS and flag_values != new_flag_values:
+ raise ValueError(
+ 'flag_values must not be customized when operating on a FlagHolder'
+ )
+ return flag_ref.name, new_flag_values
+ return flag_ref, flag_values
+
+
+def resolve_flag_refs(
+ flag_refs: Sequence[str | FlagHolder], flag_values: FlagValues
+) -> tuple[list[str], FlagValues]:
+ """Helper to validate and resolve flag reference list arguments."""
+ fv = None
+ names = []
+ for ref in flag_refs:
+ if isinstance(ref, FlagHolder):
+ newfv = ref._flagvalues # pylint: disable=protected-access
+ name = ref.name
+ else:
+ newfv = flag_values
+ name = ref
+ if fv and fv != newfv:
+ raise ValueError(
+ 'multiple FlagValues instances used in invocation. '
+ 'FlagHolders must be registered to the same FlagValues instance as '
+ 'do flag names, if provided.'
+ )
+ fv = newfv
+ names.append(name)
+ if fv is None:
+ raise ValueError('flag_refs argument must not be empty')
+ return names, fv
diff --git a/venv/Lib/site-packages/absl/flags/_helpers.py b/venv/Lib/site-packages/absl/flags/_helpers.py
new file mode 100644
index 0000000000000000000000000000000000000000..0de8153d766e99b12975a85c27e998985fd5fa96
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_helpers.py
@@ -0,0 +1,403 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Internal helper functions for Abseil Python flags library."""
+
+from collections.abc import Iterable, Sequence
+import re
+import shutil
+import sys
+import textwrap
+import types
+from typing import Any, NamedTuple
+from xml.dom import minidom
+
+
+_DEFAULT_HELP_WIDTH = 80 # Default width of help output.
+# Minimal "sane" width of help output. We assume that any value below 40 is
+# unreasonable.
+_MIN_HELP_WIDTH = 40
+
+# Define the allowed error rate in an input string to get suggestions.
+#
+# We lean towards a high threshold because we tend to be matching a phrase,
+# and the simple algorithm used here is geared towards correcting word
+# spellings.
+#
+# For manual testing, consider " --list" which produced a large number
+# of spurious suggestions when we used "least_errors > 0.5" instead of
+# "least_erros >= 0.5".
+_SUGGESTION_ERROR_RATE_THRESHOLD = 0.50
+
+# Characters that cannot appear or are highly discouraged in an XML 1.0
+# document. (See http://www.w3.org/TR/REC-xml/#charsets or
+# https://en.wikipedia.org/wiki/Valid_characters_in_XML#XML_1.0)
+_ILLEGAL_XML_CHARS_REGEX = re.compile(
+ '[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]'
+)
+
+# This is a set of module ids for the modules that disclaim key flags.
+# This module is explicitly added to this set so that we never consider it to
+# define key flag.
+disclaim_module_ids: set[int] = {id(sys.modules[__name__])}
+
+
+# Define special flags here so that help may be generated for them.
+# NOTE: Please do NOT use SPECIAL_FLAGS from outside flags module.
+# Initialized inside flagvalues.py.
+# NOTE: This cannot be annotated as its actual FlagValues type since this would
+# create a circular dependency.
+SPECIAL_FLAGS: Any = None
+
+
+# This points to the flags module, initialized in flags/__init__.py.
+# This should only be used in adopt_module_key_flags to take SPECIAL_FLAGS into
+# account.
+FLAGS_MODULE: types.ModuleType | None = None
+
+
+class _ModuleObjectAndName(NamedTuple):
+ """Module object and name.
+
+ Fields:
+ - module: object, module object.
+ - module_name: str, module name.
+ """
+ module: types.ModuleType
+ module_name: str
+
+
+def get_module_object_and_name(
+ globals_dict: dict[str, Any],
+) -> _ModuleObjectAndName | None:
+ """Returns the module that defines a global environment, and its name.
+
+ Args:
+ globals_dict: A dictionary that should correspond to an environment
+ providing the values of the globals.
+
+ Returns:
+ _ModuleObjectAndName - pair of module object & module name.
+ Returns None if the module could not be identified.
+ """
+ try:
+ name = globals_dict['__name__']
+ module = sys.modules[name]
+ except KeyError:
+ return None
+ # Pick a more informative name for the main module.
+ return _ModuleObjectAndName(
+ module, sys.argv[0] if name == '__main__' else name
+ )
+
+
+def get_calling_module_object_and_name() -> _ModuleObjectAndName:
+ """Returns the module that's calling into this module.
+
+ We generally use this function to get the name of the module calling a
+ DEFINE_foo... function.
+
+ Returns:
+ The module object that called into this one.
+
+ Raises:
+ AssertionError: Raised when no calling module could be identified.
+ """
+ for depth in range(1, sys.getrecursionlimit()):
+ # sys._getframe is the right thing to use here, as it's the best
+ # way to walk up the call stack.
+ globals_for_frame = sys._getframe(depth).f_globals # pylint: disable=protected-access
+ module = get_module_object_and_name(globals_for_frame)
+ if module is not None and id(module.module) not in disclaim_module_ids:
+ return module
+ raise AssertionError('No module was found')
+
+
+def get_calling_module() -> str:
+ """Returns the name of the module that's calling into this module."""
+ return get_calling_module_object_and_name().module_name
+
+
+def create_xml_dom_element(
+ doc: minidom.Document, name: str, value: Any
+) -> minidom.Element:
+ """Returns an XML DOM element with name and text value.
+
+ Args:
+ doc: minidom.Document, the DOM document it should create nodes from.
+ name: str, the tag of XML element.
+ value: object, whose string representation will be used
+ as the value of the XML element. Illegal or highly discouraged xml 1.0
+ characters are stripped.
+
+ Returns:
+ An instance of minidom.Element.
+ """
+ s = str(value)
+ if isinstance(value, bool):
+ # Display boolean values as the C++ flag library does: no caps.
+ s = s.lower()
+ # Remove illegal xml characters.
+ s = _ILLEGAL_XML_CHARS_REGEX.sub('', s)
+
+ e = doc.createElement(name)
+ e.appendChild(doc.createTextNode(s))
+ return e
+
+
+def get_help_width() -> int:
+ """Returns the integer width of help lines that is used in TextWrap."""
+ size = shutil.get_terminal_size(fallback=(_DEFAULT_HELP_WIDTH, 1))
+ return size.columns
+
+
+def get_flag_suggestions(
+ attempt: str, longopt_list: Sequence[str]
+) -> list[str]:
+ """Returns helpful similar matches for an invalid flag."""
+ # Don't suggest on very short strings, or if no longopts are specified.
+ if len(attempt) <= 2 or not longopt_list:
+ return []
+
+ option_names = [v.split('=')[0] for v in longopt_list]
+
+ # Find close approximations in flag prefixes.
+ # This also handles the case where the flag is spelled right but ambiguous.
+ distances = [(_damerau_levenshtein(attempt, option[0:len(attempt)]), option)
+ for option in option_names]
+ # t[0] is distance, and sorting by t[1] allows us to have stable output.
+ distances.sort()
+
+ least_errors, _ = distances[0]
+ # Don't suggest excessively bad matches.
+ if least_errors >= _SUGGESTION_ERROR_RATE_THRESHOLD * len(attempt):
+ return []
+
+ suggestions = []
+ for errors, name in distances:
+ if errors == least_errors:
+ suggestions.append(name)
+ else:
+ break
+ return suggestions
+
+
+def _damerau_levenshtein(a, b):
+ """Returns Damerau-Levenshtein edit distance from a to b."""
+ memo = {}
+
+ def distance(x, y):
+ """Recursively defined string distance with memoization."""
+ if (x, y) in memo:
+ return memo[x, y]
+ if not x:
+ d = len(y)
+ elif not y:
+ d = len(x)
+ else:
+ d = min(
+ distance(x[1:], y) + 1, # correct an insertion error
+ distance(x, y[1:]) + 1, # correct a deletion error
+ distance(x[1:], y[1:]) + (x[0] != y[0])) # correct a wrong character
+ if len(x) >= 2 and len(y) >= 2 and x[0] == y[1] and x[1] == y[0]:
+ # Correct a transposition.
+ t = distance(x[2:], y[2:]) + 1
+ if d > t:
+ d = t
+
+ memo[x, y] = d
+ return d
+ return distance(a, b)
+
+
+def text_wrap(
+ text: str,
+ length: int | None = None,
+ indent: str = '',
+ firstline_indent: str | None = None,
+) -> str:
+ """Wraps a given text to a maximum line length and returns it.
+
+ It turns lines that only contain whitespace into empty lines, keeps new lines,
+ and expands tabs using 4 spaces.
+
+ Args:
+ text: Text to wrap.
+ length: Maximum length of a line, includes indentation. If this is `None`
+ then use `get_help_width()`.
+ indent: Indent for all but first line.
+ firstline_indent: Indent for first line. If `None`, fall back to `indent`.
+
+ Returns:
+ The wrapped text.
+
+ Raises:
+ ValueError: Raised if indent or firstline_indent not shorter than length.
+ """
+ # Get defaults where callee used None
+ if length is None:
+ length = get_help_width()
+ if indent is None:
+ indent = ''
+ if firstline_indent is None:
+ firstline_indent = indent
+
+ if len(indent) >= length:
+ raise ValueError('Length of indent exceeds length')
+ if len(firstline_indent) >= length:
+ raise ValueError('Length of first line indent exceeds length')
+
+ text = text.expandtabs(4)
+
+ result = []
+ # Create one wrapper for the first paragraph and one for subsequent
+ # paragraphs that does not have the initial wrapping.
+ wrapper = textwrap.TextWrapper(
+ width=length, initial_indent=firstline_indent, subsequent_indent=indent)
+ subsequent_wrapper = textwrap.TextWrapper(
+ width=length, initial_indent=indent, subsequent_indent=indent)
+
+ # textwrap does not have any special treatment for newlines. From the docs:
+ # "...newlines may appear in the middle of a line and cause strange output.
+ # For this reason, text should be split into paragraphs (using
+ # str.splitlines() or similar) which are wrapped separately."
+ for paragraph in (p.strip() for p in text.splitlines()):
+ if paragraph:
+ result.extend(wrapper.wrap(paragraph))
+ else:
+ result.append('') # Keep empty lines.
+ # Replace initial wrapper with wrapper for subsequent paragraphs.
+ wrapper = subsequent_wrapper
+
+ return '\n'.join(result)
+
+
+def flag_dict_to_args(
+ flag_map: dict[str, Any], multi_flags: set[str] | None = None
+) -> Iterable[str]:
+ """Convert a dict of values into process call parameters.
+
+ This method is used to convert a dictionary into a sequence of parameters
+ for a binary that parses arguments using this module.
+
+ Args:
+ flag_map: dict, a mapping where the keys are flag names (strings).
+ values are treated according to their type:
+
+ * If value is ``None``, then only the name is emitted.
+ * If value is ``True``, then only the name is emitted.
+ * If value is ``False``, then only the name prepended with 'no' is
+ emitted.
+ * If value is a string then ``--name=value`` is emitted.
+ * If value is a collection, this will emit
+ ``--name=value1,value2,value3``, unless the flag name is in
+ ``multi_flags``, in which case this will emit
+ ``--name=value1 --name=value2 --name=value3``.
+ * Everything else is converted to string an passed as such.
+
+ multi_flags: set, names (strings) of flags that should be treated as
+ multi-flags.
+ Yields:
+ sequence of string suitable for a subprocess execution.
+ """
+ for key, value in flag_map.items():
+ if value is None:
+ yield '--%s' % key
+ elif isinstance(value, bool):
+ if value:
+ yield '--%s' % key
+ else:
+ yield '--no%s' % key
+ elif isinstance(value, (bytes, str)):
+ # We don't want strings to be handled like python collections.
+ yield '--%s=%s' % (key, value) # type: ignore[str-bytes-safe]
+ else:
+ # Now we attempt to deal with collections.
+ try:
+ if multi_flags and key in multi_flags:
+ for item in value:
+ yield '--%s=%s' % (key, str(item))
+ else:
+ yield '--%s=%s' % (key, ','.join(str(item) for item in value))
+ except TypeError:
+ # Default case.
+ yield '--%s=%s' % (key, value)
+
+
+def trim_docstring(docstring: str) -> str:
+ """Removes indentation from triple-quoted strings.
+
+ This is the function specified in PEP 257 to handle docstrings:
+ https://www.python.org/dev/peps/pep-0257/.
+
+ Args:
+ docstring: str, a python docstring.
+
+ Returns:
+ str, docstring with indentation removed.
+ """
+ if not docstring:
+ return ''
+
+ # If you've got a line longer than this you have other problems...
+ max_indent = 1 << 29
+
+ # Convert tabs to spaces (following the normal Python rules)
+ # and split into a list of lines:
+ lines = docstring.expandtabs().splitlines()
+
+ # Determine minimum indentation (first line doesn't count):
+ indent = max_indent
+ for line in lines[1:]:
+ stripped = line.lstrip()
+ if stripped:
+ indent = min(indent, len(line) - len(stripped))
+ # Remove indentation (first line is special):
+ trimmed = [lines[0].strip()]
+ if indent < max_indent:
+ for line in lines[1:]:
+ trimmed.append(line[indent:].rstrip())
+ # Strip off trailing and leading blank lines:
+ while trimmed and not trimmed[-1]:
+ trimmed.pop()
+ while trimmed and not trimmed[0]:
+ trimmed.pop(0)
+ # Return a single string:
+ return '\n'.join(trimmed)
+
+
+def doc_to_help(doc: str) -> str:
+ """Takes a __doc__ string and reformats it as help."""
+
+ # Get rid of starting and ending white space. Using lstrip() or even
+ # strip() could drop more than maximum of first line and right space
+ # of last line.
+ doc = doc.strip()
+
+ # Get rid of all empty lines.
+ whitespace_only_line = re.compile('^[ \t]+$', re.M)
+ doc = whitespace_only_line.sub('', doc)
+
+ # Cut out common space at line beginnings.
+ doc = trim_docstring(doc)
+
+ # Just like this module's comment, comments tend to be aligned somehow.
+ # In other words they all start with the same amount of white space.
+ # 1) keep double new lines;
+ # 2) keep ws after new lines if not empty line;
+ # 3) all other new lines shall be changed to a space;
+ # Solution: Match new lines between non white space and replace with space.
+ doc = re.sub(r'(?<=\S)\n(?=\S)', ' ', doc, flags=re.M)
+
+ return doc
diff --git a/venv/Lib/site-packages/absl/flags/_validators.py b/venv/Lib/site-packages/absl/flags/_validators.py
new file mode 100644
index 0000000000000000000000000000000000000000..d4a937acd6184a02366c11384f7e8421cdbc1af1
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_validators.py
@@ -0,0 +1,353 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Module to enforce different constraints on flags.
+
+Flags validators can be registered using following functions / decorators::
+
+ flags.register_validator
+ @flags.validator
+ flags.register_multi_flags_validator
+ @flags.multi_flags_validator
+
+Three convenience functions are also provided for common flag constraints::
+
+ flags.mark_flag_as_required
+ flags.mark_flags_as_required
+ flags.mark_flags_as_mutual_exclusive
+ flags.mark_bool_flags_as_mutual_exclusive
+
+See their docstring in this module for a usage manual.
+
+Do NOT import this module directly. Import the flags package and use the
+aliases defined at the package level instead.
+"""
+
+import warnings
+
+from absl.flags import _exceptions
+from absl.flags import _flagvalues
+from absl.flags import _validators_classes
+
+
+def register_validator(flag_name,
+ checker,
+ message='Flag validation failed',
+ flag_values=_flagvalues.FLAGS):
+ """Adds a constraint, which will be enforced during program execution.
+
+ The constraint is validated when flags are initially parsed, and after each
+ change of the corresponding flag's value.
+
+ Args:
+ flag_name: str | FlagHolder, name or holder of the flag to be checked.
+ Positional-only parameter.
+ checker: callable, a function to validate the flag.
+
+ * input - A single positional argument: The value of the corresponding
+ flag (string, boolean, etc. This value will be passed to checker
+ by the library).
+ * output - bool, True if validator constraint is satisfied.
+ If constraint is not satisfied, it should either ``return False`` or
+ ``raise flags.ValidationError(desired_error_message)``.
+
+ message: str, error text to be shown to the user if checker returns False.
+ If checker raises flags.ValidationError, message from the raised
+ error will be shown.
+ flag_values: flags.FlagValues, optional FlagValues instance to validate
+ against.
+
+ Raises:
+ AttributeError: Raised when flag_name is not registered as a valid flag
+ name.
+ ValueError: Raised when flag_values is non-default and does not match the
+ FlagValues of the provided FlagHolder instance.
+ """
+ flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
+ v = _validators_classes.SingleFlagValidator(flag_name, checker, message)
+ _add_validator(flag_values, v)
+
+
+def validator(flag_name, message='Flag validation failed',
+ flag_values=_flagvalues.FLAGS):
+ """A function decorator for defining a flag validator.
+
+ Registers the decorated function as a validator for flag_name, e.g.::
+
+ @flags.validator('foo')
+ def _CheckFoo(foo):
+ ...
+
+ See :func:`register_validator` for the specification of checker function.
+
+ Args:
+ flag_name: str | FlagHolder, name or holder of the flag to be checked.
+ Positional-only parameter.
+ message: str, error text to be shown to the user if checker returns False.
+ If checker raises flags.ValidationError, message from the raised
+ error will be shown.
+ flag_values: flags.FlagValues, optional FlagValues instance to validate
+ against.
+ Returns:
+ A function decorator that registers its function argument as a validator.
+ Raises:
+ AttributeError: Raised when flag_name is not registered as a valid flag
+ name.
+ """
+
+ def decorate(function):
+ register_validator(flag_name, function,
+ message=message,
+ flag_values=flag_values)
+ return function
+ return decorate
+
+
+def register_multi_flags_validator(flag_names,
+ multi_flags_checker,
+ message='Flags validation failed',
+ flag_values=_flagvalues.FLAGS):
+ """Adds a constraint to multiple flags.
+
+ The constraint is validated when flags are initially parsed, and after each
+ change of the corresponding flag's value.
+
+ Args:
+ flag_names: [str | FlagHolder], a list of the flag names or holders to be
+ checked. Positional-only parameter.
+ multi_flags_checker: callable, a function to validate the flag.
+
+ * input - dict, with keys() being flag_names, and value for each key
+ being the value of the corresponding flag (string, boolean, etc).
+ * output - bool, True if validator constraint is satisfied.
+ If constraint is not satisfied, it should either return False or
+ raise flags.ValidationError.
+
+ message: str, error text to be shown to the user if checker returns False.
+ If checker raises flags.ValidationError, message from the raised
+ error will be shown.
+ flag_values: flags.FlagValues, optional FlagValues instance to validate
+ against.
+
+ Raises:
+ AttributeError: Raised when a flag is not registered as a valid flag name.
+ ValueError: Raised when multiple FlagValues are used in the same
+ invocation. This can occur when FlagHolders have different `_flagvalues`
+ or when str-type flag_names entries are present and the `flag_values`
+ argument does not match that of provided FlagHolder(s).
+ """
+ flag_names, flag_values = _flagvalues.resolve_flag_refs(
+ flag_names, flag_values)
+ v = _validators_classes.MultiFlagsValidator(
+ flag_names, multi_flags_checker, message)
+ _add_validator(flag_values, v)
+
+
+def multi_flags_validator(flag_names,
+ message='Flag validation failed',
+ flag_values=_flagvalues.FLAGS):
+ """A function decorator for defining a multi-flag validator.
+
+ Registers the decorated function as a validator for flag_names, e.g.::
+
+ @flags.multi_flags_validator(['foo', 'bar'])
+ def _CheckFooBar(flags_dict):
+ ...
+
+ See :func:`register_multi_flags_validator` for the specification of checker
+ function.
+
+ Args:
+ flag_names: [str | FlagHolder], a list of the flag names or holders to be
+ checked. Positional-only parameter.
+ message: str, error text to be shown to the user if checker returns False.
+ If checker raises flags.ValidationError, message from the raised
+ error will be shown.
+ flag_values: flags.FlagValues, optional FlagValues instance to validate
+ against.
+
+ Returns:
+ A function decorator that registers its function argument as a validator.
+
+ Raises:
+ AttributeError: Raised when a flag is not registered as a valid flag name.
+ """
+
+ def decorate(function):
+ register_multi_flags_validator(flag_names,
+ function,
+ message=message,
+ flag_values=flag_values)
+ return function
+
+ return decorate
+
+
+def mark_flag_as_required(flag_name, flag_values=_flagvalues.FLAGS):
+ """Ensures that flag is not None during program execution.
+
+ Registers a flag validator, which will follow usual validator rules.
+ Important note: validator will pass for any non-``None`` value, such as
+ ``False``, ``0`` (zero), ``''`` (empty string) and so on.
+
+ If your module might be imported by others, and you only wish to make the flag
+ required when the module is directly executed, call this method like this::
+
+ if __name__ == '__main__':
+ flags.mark_flag_as_required('your_flag_name')
+ app.run()
+
+ Args:
+ flag_name: str | FlagHolder, name or holder of the flag.
+ Positional-only parameter.
+ flag_values: flags.FlagValues, optional :class:`~absl.flags.FlagValues`
+ instance where the flag is defined.
+ Raises:
+ AttributeError: Raised when flag_name is not registered as a valid flag
+ name.
+ ValueError: Raised when flag_values is non-default and does not match the
+ FlagValues of the provided FlagHolder instance.
+ """
+ flag_name, flag_values = _flagvalues.resolve_flag_ref(flag_name, flag_values)
+ if flag_values[flag_name].default is not None:
+ warnings.warn(
+ 'Flag --%s has a non-None default value; therefore, '
+ 'mark_flag_as_required will pass even if flag is not specified in the '
+ 'command line!' % flag_name,
+ stacklevel=2)
+ register_validator(
+ flag_name,
+ lambda value: value is not None,
+ message=f'Flag --{flag_name} must have a value other than None.',
+ flag_values=flag_values,
+ )
+
+
+def mark_flags_as_required(flag_names, flag_values=_flagvalues.FLAGS):
+ """Ensures that flags are not None during program execution.
+
+ If your module might be imported by others, and you only wish to make the flag
+ required when the module is directly executed, call this method like this::
+
+ if __name__ == '__main__':
+ flags.mark_flags_as_required(['flag1', 'flag2', 'flag3'])
+ app.run()
+
+ Args:
+ flag_names: Sequence[str | FlagHolder], names or holders of the flags.
+ flag_values: flags.FlagValues, optional FlagValues instance where the flags
+ are defined.
+ Raises:
+ AttributeError: If any of flag name has not already been defined as a flag.
+ """
+ for flag_name in flag_names:
+ mark_flag_as_required(flag_name, flag_values)
+
+
+def mark_flags_as_mutual_exclusive(flag_names, required=False,
+ flag_values=_flagvalues.FLAGS):
+ """Ensures that only one flag among flag_names is not None.
+
+ Important note: This validator checks if flag values are ``None``, and it does
+ not distinguish between default and explicit values. Therefore, this validator
+ does not make sense when applied to flags with default values other than None,
+ including other false values (e.g. ``False``, ``0``, ``''``, ``[]``). That
+ includes multi flags with a default value of ``[]`` instead of None.
+
+ Args:
+ flag_names: [str | FlagHolder], names or holders of flags.
+ Positional-only parameter.
+ required: bool. If true, exactly one of the flags must have a value other
+ than None. Otherwise, at most one of the flags can have a value other
+ than None, and it is valid for all of the flags to be None.
+ flag_values: flags.FlagValues, optional FlagValues instance where the flags
+ are defined.
+
+ Raises:
+ ValueError: Raised when multiple FlagValues are used in the same
+ invocation. This can occur when FlagHolders have different `_flagvalues`
+ or when str-type flag_names entries are present and the `flag_values`
+ argument does not match that of provided FlagHolder(s).
+ """
+ flag_names, flag_values = _flagvalues.resolve_flag_refs(
+ flag_names, flag_values)
+ for flag_name in flag_names:
+ if flag_values[flag_name].default is not None:
+ warnings.warn(
+ 'Flag --{} has a non-None default value. That does not make sense '
+ 'with mark_flags_as_mutual_exclusive, which checks whether the '
+ 'listed flags have a value other than None.'.format(flag_name),
+ stacklevel=2)
+
+ def validate_mutual_exclusion(flags_dict):
+ flag_count = sum(1 for val in flags_dict.values() if val is not None)
+ if flag_count == 1 or (not required and flag_count == 0):
+ return True
+ raise _exceptions.ValidationError(
+ '{} one of ({}) must have a value other than None.'.format(
+ 'Exactly' if required else 'At most', ', '.join(flag_names)))
+
+ register_multi_flags_validator(
+ flag_names, validate_mutual_exclusion, flag_values=flag_values)
+
+
+def mark_bool_flags_as_mutual_exclusive(flag_names, required=False,
+ flag_values=_flagvalues.FLAGS):
+ """Ensures that only one flag among flag_names is True.
+
+ Args:
+ flag_names: [str | FlagHolder], names or holders of flags.
+ Positional-only parameter.
+ required: bool. If true, exactly one flag must be True. Otherwise, at most
+ one flag can be True, and it is valid for all flags to be False.
+ flag_values: flags.FlagValues, optional FlagValues instance where the flags
+ are defined.
+
+ Raises:
+ ValueError: Raised when multiple FlagValues are used in the same
+ invocation. This can occur when FlagHolders have different `_flagvalues`
+ or when str-type flag_names entries are present and the `flag_values`
+ argument does not match that of provided FlagHolder(s).
+ """
+ flag_names, flag_values = _flagvalues.resolve_flag_refs(
+ flag_names, flag_values)
+ for flag_name in flag_names:
+ if not flag_values[flag_name].boolean:
+ raise _exceptions.ValidationError(
+ 'Flag --{} is not Boolean, which is required for flags used in '
+ 'mark_bool_flags_as_mutual_exclusive.'.format(flag_name))
+
+ def validate_boolean_mutual_exclusion(flags_dict):
+ flag_count = sum(bool(val) for val in flags_dict.values())
+ if flag_count == 1 or (not required and flag_count == 0):
+ return True
+ raise _exceptions.ValidationError(
+ '{} one of ({}) must be True.'.format(
+ 'Exactly' if required else 'At most', ', '.join(flag_names)))
+
+ register_multi_flags_validator(
+ flag_names, validate_boolean_mutual_exclusion, flag_values=flag_values)
+
+
+def _add_validator(fv, validator_instance):
+ """Register new flags validator to be checked.
+
+ Args:
+ fv: flags.FlagValues, the FlagValues instance to add the validator.
+ validator_instance: validators.Validator, the validator to add.
+ Raises:
+ KeyError: Raised when validators work with a non-existing flag.
+ """
+ for flag_name in validator_instance.get_flags_names():
+ fv[flag_name].validators.append(validator_instance)
diff --git a/venv/Lib/site-packages/absl/flags/_validators_classes.py b/venv/Lib/site-packages/absl/flags/_validators_classes.py
new file mode 100644
index 0000000000000000000000000000000000000000..cf978bfd1bf837895310ae0b5e1b3eed90fef375
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/_validators_classes.py
@@ -0,0 +1,172 @@
+# Copyright 2021 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Defines *private* classes used for flag validators.
+
+Do NOT import this module. DO NOT use anything from this module. They are
+private APIs.
+"""
+
+from absl.flags import _exceptions
+
+
+class Validator:
+ """Base class for flags validators.
+
+ Users should NOT overload these classes, and use flags.Register...
+ methods instead.
+ """
+
+ # Used to assign each validator an unique insertion_index
+ validators_count = 0
+
+ def __init__(self, checker, message):
+ """Constructor to create all validators.
+
+ Args:
+ checker: function to verify the constraint.
+ Input of this method varies, see SingleFlagValidator and
+ multi_flags_validator for a detailed description.
+ message: str, error message to be shown to the user.
+ """
+ self.checker = checker
+ self.message = message
+ Validator.validators_count += 1
+ # Used to assert validators in the order they were registered.
+ self.insertion_index = Validator.validators_count
+
+ def verify(self, flag_values):
+ """Verifies that constraint is satisfied.
+
+ flags library calls this method to verify Validator's constraint.
+
+ Args:
+ flag_values: flags.FlagValues, the FlagValues instance to get flags from.
+ Raises:
+ Error: Raised if constraint is not satisfied.
+ """
+ param = self._get_input_to_checker_function(flag_values)
+ if not self.checker(param):
+ raise _exceptions.ValidationError(self.message)
+
+ def get_flags_names(self):
+ """Returns the names of the flags checked by this validator.
+
+ Returns:
+ [string], names of the flags.
+ """
+ raise NotImplementedError('This method should be overloaded')
+
+ def print_flags_with_values(self, flag_values):
+ raise NotImplementedError('This method should be overloaded')
+
+ def _get_input_to_checker_function(self, flag_values):
+ """Given flag values, returns the input to be given to checker.
+
+ Args:
+ flag_values: flags.FlagValues, containing all flags.
+ Returns:
+ The input to be given to checker. The return type depends on the specific
+ validator.
+ """
+ raise NotImplementedError('This method should be overloaded')
+
+
+class SingleFlagValidator(Validator):
+ """Validator behind register_validator() method.
+
+ Validates that a single flag passes its checker function. The checker function
+ takes the flag value and returns True (if value looks fine) or, if flag value
+ is not valid, either returns False or raises an Exception.
+ """
+
+ def __init__(self, flag_name, checker, message):
+ """Constructor.
+
+ Args:
+ flag_name: string, name of the flag.
+ checker: function to verify the validator.
+ input - value of the corresponding flag (string, boolean, etc).
+ output - bool, True if validator constraint is satisfied.
+ If constraint is not satisfied, it should either return False or
+ raise flags.ValidationError(desired_error_message).
+ message: str, error message to be shown to the user if validator's
+ condition is not satisfied.
+ """
+ super().__init__(checker, message)
+ self.flag_name = flag_name
+
+ def get_flags_names(self):
+ return [self.flag_name]
+
+ def print_flags_with_values(self, flag_values):
+ return 'flag --%s=%s' % (self.flag_name, flag_values[self.flag_name].value)
+
+ def _get_input_to_checker_function(self, flag_values):
+ """Given flag values, returns the input to be given to checker.
+
+ Args:
+ flag_values: flags.FlagValues, the FlagValues instance to get flags from.
+ Returns:
+ object, the input to be given to checker.
+ """
+ return flag_values[self.flag_name].value
+
+
+class MultiFlagsValidator(Validator):
+ """Validator behind register_multi_flags_validator method.
+
+ Validates that flag values pass their common checker function. The checker
+ function takes flag values and returns True (if values look fine) or,
+ if values are not valid, either returns False or raises an Exception.
+ """
+
+ def __init__(self, flag_names, checker, message):
+ """Constructor.
+
+ Args:
+ flag_names: [str], containing names of the flags used by checker.
+ checker: function to verify the validator.
+ input - dict, with keys() being flag_names, and value for each
+ key being the value of the corresponding flag (string, boolean,
+ etc).
+ output - bool, True if validator constraint is satisfied.
+ If constraint is not satisfied, it should either return False or
+ raise flags.ValidationError(desired_error_message).
+ message: str, error message to be shown to the user if validator's
+ condition is not satisfied
+ """
+ super().__init__(checker, message)
+ self.flag_names = flag_names
+
+ def _get_input_to_checker_function(self, flag_values):
+ """Given flag values, returns the input to be given to checker.
+
+ Args:
+ flag_values: flags.FlagValues, the FlagValues instance to get flags from.
+ Returns:
+ dict, with keys() being self.flag_names, and value for each key
+ being the value of the corresponding flag (string, boolean, etc).
+ """
+ return {key: flag_values[key].value for key in self.flag_names}
+
+ def print_flags_with_values(self, flag_values):
+ prefix = 'flags '
+ flags_with_values = []
+ for key in self.flag_names:
+ flags_with_values.append('%s=%s' % (key, flag_values[key].value))
+ return prefix + ', '.join(flags_with_values)
+
+ def get_flags_names(self):
+ return self.flag_names
diff --git a/venv/Lib/site-packages/absl/flags/argparse_flags.py b/venv/Lib/site-packages/absl/flags/argparse_flags.py
new file mode 100644
index 0000000000000000000000000000000000000000..9f77690f10c1e0fc9619b857ba6169636f30e6c3
--- /dev/null
+++ b/venv/Lib/site-packages/absl/flags/argparse_flags.py
@@ -0,0 +1,390 @@
+# Copyright 2018 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""This module provides argparse integration with absl.flags.
+
+``argparse_flags.ArgumentParser`` is a drop-in replacement for
+:class:`argparse.ArgumentParser`. It takes care of collecting and defining absl
+flags in :mod:`argparse`.
+
+Here is a simple example::
+
+ # Assume the following absl.flags is defined in another module:
+ #
+ # from absl import flags
+ # flags.DEFINE_string('echo', None, 'The echo message.')
+ #
+ parser = argparse_flags.ArgumentParser(
+ description='A demo of absl.flags and argparse integration.')
+ parser.add_argument('--header', help='Header message to print.')
+
+ # The parser will also accept the absl flag `--echo`.
+ # The `header` value is available as `args.header` just like a regular
+ # argparse flag. The absl flag `--echo` continues to be available via
+ # `absl.flags.FLAGS` if you want to access it.
+ args = parser.parse_args()
+
+ # Example usages:
+ # ./program --echo='A message.' --header='A header'
+ # ./program --header 'A header' --echo 'A message.'
+
+
+Here is another example demonstrates subparsers::
+
+ parser = argparse_flags.ArgumentParser(description='A subcommands demo.')
+ parser.add_argument('--header', help='The header message to print.')
+
+ subparsers = parser.add_subparsers(help='The command to execute.')
+
+ roll_dice_parser = subparsers.add_parser(
+ 'roll_dice', help='Roll a dice.',
+ # By default, absl flags can also be specified after the sub-command.
+ # To only allow them before sub-command, pass
+ # `inherited_absl_flags=None`.
+ inherited_absl_flags=None)
+ roll_dice_parser.add_argument('--num_faces', type=int, default=6)
+ roll_dice_parser.set_defaults(command=roll_dice)
+
+ shuffle_parser = subparsers.add_parser('shuffle', help='Shuffle inputs.')
+ shuffle_parser.add_argument(
+ 'inputs', metavar='I', nargs='+', help='Inputs to shuffle.')
+ shuffle_parser.set_defaults(command=shuffle)
+
+ args = parser.parse_args(argv[1:])
+ args.command(args)
+
+ # Example usages:
+ # ./program --echo='A message.' roll_dice --num_faces=6
+ # ./program shuffle --echo='A message.' 1 2 3 4
+
+
+There are several differences between :mod:`absl.flags` and
+:mod:`~absl.flags.argparse_flags`:
+
+1. Flags defined with absl.flags are parsed differently when using the
+ argparse parser. Notably:
+
+ 1) absl.flags allows both single-dash and double-dash for any flag, and
+ doesn't distinguish them; argparse_flags only allows double-dash for
+ flag's regular name, and single-dash for flag's ``short_name``.
+ 2) Boolean flags in absl.flags can be specified with ``--bool``,
+ ``--nobool``, as well as ``--bool=true/false`` (though not recommended);
+ in argparse_flags, it only allows ``--bool``, ``--nobool``.
+
+2. Help related flag differences:
+
+ 1) absl.flags does not define help flags, absl.app does that; argparse_flags
+ defines help flags unless passed with ``add_help=False``.
+ 2) absl.app supports ``--helpxml``; argparse_flags does not.
+ 3) argparse_flags supports ``-h``; absl.app does not.
+"""
+
+import argparse
+import sys
+
+from absl import flags
+
+
+_BUILT_IN_FLAGS = frozenset({
+ 'help',
+ 'helpshort',
+ 'helpfull',
+ 'helpxml',
+ 'flagfile',
+ 'undefok',
+})
+
+
+class ArgumentParser(argparse.ArgumentParser):
+ """Custom ArgumentParser class to support special absl flags."""
+
+ def __init__(self, **kwargs):
+ """Initializes ArgumentParser.
+
+ Args:
+ **kwargs: same as argparse.ArgumentParser, except:
+ 1. It also accepts `inherited_absl_flags`: the absl flags to inherit.
+ The default is the global absl.flags.FLAGS instance. Pass None to
+ ignore absl flags.
+ 2. The `prefix_chars` argument must be the default value '-'.
+
+ Raises:
+ ValueError: Raised when prefix_chars is not '-'.
+ """
+ prefix_chars = kwargs.get('prefix_chars', '-')
+ if prefix_chars != '-':
+ raise ValueError(
+ 'argparse_flags.ArgumentParser only supports "-" as the prefix '
+ 'character, found "{}".'.format(prefix_chars))
+
+ # Remove inherited_absl_flags before calling super.
+ self._inherited_absl_flags = kwargs.pop('inherited_absl_flags', flags.FLAGS)
+ # Now call super to initialize argparse.ArgumentParser before calling
+ # add_argument in _define_absl_flags.
+ super().__init__(**kwargs)
+
+ if self.add_help:
+ # -h and --help are defined in super.
+ # Also add the --helpshort and --helpfull flags.
+ self.add_argument(
+ # Action 'help' defines a similar flag to -h/--help.
+ '--helpshort', action='help',
+ default=argparse.SUPPRESS, help=argparse.SUPPRESS)
+ self.add_argument(
+ '--helpfull', action=_HelpFullAction,
+ default=argparse.SUPPRESS, help='show full help message and exit')
+
+ if self._inherited_absl_flags is not None:
+ self.add_argument(
+ '--undefok', default=argparse.SUPPRESS, help=argparse.SUPPRESS)
+ self._define_absl_flags(self._inherited_absl_flags)
+
+ def parse_known_args(self, args=None, namespace=None):
+ if args is None:
+ args = sys.argv[1:]
+ if self._inherited_absl_flags is not None:
+ # Handle --flagfile.
+ # Explicitly specify force_gnu=True, since argparse behaves like
+ # gnu_getopt: flags can be specified after positional arguments.
+ args = self._inherited_absl_flags.read_flags_from_files(
+ args, force_gnu=True)
+
+ undefok_missing = object()
+ undefok = getattr(namespace, 'undefok', undefok_missing)
+
+ namespace, args = super().parse_known_args(args, namespace)
+
+ # For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
+ # sub-parsers don't preserve existing namespace attributes.
+ # Restore the undefok attribute if a sub-parser dropped it.
+ if undefok is not undefok_missing:
+ namespace.undefok = undefok
+
+ if self._inherited_absl_flags is not None:
+ # Handle --undefok. At this point, `args` only contains unknown flags,
+ # so it won't strip defined flags that are also specified with --undefok.
+ # For Python <= 2.7.8: https://bugs.python.org/issue9351, a bug where
+ # sub-parsers don't preserve existing namespace attributes. The undefok
+ # attribute might not exist because a subparser dropped it.
+ if hasattr(namespace, 'undefok'):
+ args = _strip_undefok_args(namespace.undefok, args)
+ # absl flags are not exposed in the Namespace object. See Namespace:
+ # https://docs.python.org/3/library/argparse.html#argparse.Namespace.
+ del namespace.undefok
+ self._inherited_absl_flags.mark_as_parsed()
+ try:
+ self._inherited_absl_flags.validate_all_flags()
+ except flags.IllegalFlagValueError as e:
+ self.error(str(e))
+
+ return namespace, args
+
+ def _define_absl_flags(self, absl_flags):
+ """Defines flags from absl_flags."""
+ key_flags = set(absl_flags.get_key_flags_for_module(sys.argv[0]))
+ for name in absl_flags:
+ if name in _BUILT_IN_FLAGS:
+ # Do not inherit built-in flags.
+ continue
+ flag_instance = absl_flags[name]
+ # Each flags with short_name appears in FLAGS twice, so only define
+ # when the dictionary key is equal to the regular name.
+ if name == flag_instance.name:
+ # Suppress the flag in the help short message if it's not a main
+ # module's key flag.
+ suppress = flag_instance not in key_flags
+ self._define_absl_flag(flag_instance, suppress)
+
+ def _define_absl_flag(self, flag_instance, suppress):
+ """Defines a flag from the flag_instance."""
+ flag_name = flag_instance.name
+ short_name = flag_instance.short_name
+ argument_names = ['--' + flag_name]
+ if short_name:
+ argument_names.insert(0, '-' + short_name)
+ if suppress:
+ helptext = argparse.SUPPRESS
+ else:
+ # argparse help string uses %-formatting. Escape the literal %'s.
+ helptext = flag_instance.help.replace('%', '%%')
+ if flag_instance.boolean:
+ # Only add the `no` form to the long name.
+ argument_names.append('--no' + flag_name)
+ self.add_argument(
+ *argument_names, action=_BooleanFlagAction, help=helptext,
+ metavar=flag_instance.name.upper(),
+ flag_instance=flag_instance)
+ else:
+ self.add_argument(
+ *argument_names, action=_FlagAction, help=helptext,
+ metavar=flag_instance.name.upper(),
+ flag_instance=flag_instance)
+
+
+class _FlagAction(argparse.Action):
+ """Action class for Abseil non-boolean flags."""
+
+ def __init__(
+ self,
+ option_strings,
+ dest,
+ help, # pylint: disable=redefined-builtin
+ metavar,
+ flag_instance,
+ default=argparse.SUPPRESS):
+ """Initializes _FlagAction.
+
+ Args:
+ option_strings: See argparse.Action.
+ dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
+ help: See argparse.Action.
+ metavar: See argparse.Action.
+ flag_instance: absl.flags.Flag, the absl flag instance.
+ default: Ignored. The flag always uses dest=argparse.SUPPRESS so it
+ doesn't affect the parsing result.
+ """
+ del dest
+ self._flag_instance = flag_instance
+ super().__init__(
+ option_strings=option_strings,
+ dest=argparse.SUPPRESS,
+ help=help,
+ metavar=metavar,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """See https://docs.python.org/3/library/argparse.html#action-classes."""
+ self._flag_instance.parse(values)
+ self._flag_instance.using_default_value = False
+
+
+class _BooleanFlagAction(argparse.Action):
+ """Action class for Abseil boolean flags."""
+
+ def __init__(
+ self,
+ option_strings,
+ dest,
+ help, # pylint: disable=redefined-builtin
+ metavar,
+ flag_instance,
+ default=argparse.SUPPRESS):
+ """Initializes _BooleanFlagAction.
+
+ Args:
+ option_strings: See argparse.Action.
+ dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
+ help: See argparse.Action.
+ metavar: See argparse.Action.
+ flag_instance: absl.flags.Flag, the absl flag instance.
+ default: Ignored. The flag always uses dest=argparse.SUPPRESS so it
+ doesn't affect the parsing result.
+ """
+ del dest, default
+ self._flag_instance = flag_instance
+ flag_names = [self._flag_instance.name]
+ if self._flag_instance.short_name:
+ flag_names.append(self._flag_instance.short_name)
+ self._flag_names = frozenset(flag_names)
+ super().__init__(
+ option_strings=option_strings,
+ dest=argparse.SUPPRESS,
+ nargs=0, # Does not accept values, only `--bool` or `--nobool`.
+ help=help,
+ metavar=metavar,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """See https://docs.python.org/3/library/argparse.html#action-classes."""
+ if not isinstance(values, list) or values:
+ raise ValueError('values must be an empty list.')
+ if option_string.startswith('--'):
+ option = option_string[2:]
+ else:
+ option = option_string[1:]
+ if option in self._flag_names:
+ self._flag_instance.parse('true')
+ else:
+ if not option.startswith('no') or option[2:] not in self._flag_names:
+ raise ValueError('invalid option_string: ' + option_string)
+ self._flag_instance.parse('false')
+ self._flag_instance.using_default_value = False
+
+
+class _HelpFullAction(argparse.Action):
+ """Action class for --helpfull flag."""
+
+ def __init__(self, option_strings, dest, default, help): # pylint: disable=redefined-builtin
+ """Initializes _HelpFullAction.
+
+ Args:
+ option_strings: See argparse.Action.
+ dest: Ignored. The flag is always defined with dest=argparse.SUPPRESS.
+ default: Ignored.
+ help: See argparse.Action.
+ """
+ del dest, default
+ super().__init__(
+ option_strings=option_strings,
+ dest=argparse.SUPPRESS,
+ default=argparse.SUPPRESS,
+ nargs=0,
+ help=help,
+ )
+
+ def __call__(self, parser, namespace, values, option_string=None):
+ """See https://docs.python.org/3/library/argparse.html#action-classes."""
+ # This only prints flags when help is not argparse.SUPPRESS.
+ # It includes user defined argparse flags, as well as main module's
+ # key absl flags. Other absl flags use argparse.SUPPRESS, so they aren't
+ # printed here.
+ parser.print_help()
+
+ absl_flags = parser._inherited_absl_flags # pylint: disable=protected-access
+ if absl_flags is not None:
+ modules = sorted(absl_flags.flags_by_module_dict())
+ main_module = sys.argv[0]
+ if main_module in modules:
+ # The main module flags are already printed in parser.print_help().
+ modules.remove(main_module)
+ print(absl_flags._get_help_for_modules( # pylint: disable=protected-access
+ modules, prefix='', include_special_flags=True))
+ parser.exit()
+
+
+def _strip_undefok_args(undefok, args):
+ """Returns a new list of args after removing flags in --undefok."""
+ if undefok:
+ undefok_names = {name.strip() for name in undefok.split(',')}
+ undefok_names |= {'no' + name for name in undefok_names}
+ # Remove undefok flags.
+ args = [arg for arg in args if not _is_undefok(arg, undefok_names)]
+ return args
+
+
+def _is_undefok(arg, undefok_names):
+ """Returns whether we can ignore arg based on a set of undefok flag names."""
+ if not arg.startswith('-'):
+ return False
+ if arg.startswith('--'):
+ arg_without_dash = arg[2:]
+ else:
+ arg_without_dash = arg[1:]
+ if '=' in arg_without_dash:
+ name, _ = arg_without_dash.split('=', 1)
+ else:
+ name = arg_without_dash
+ if name in undefok_names:
+ return True
+ return False
diff --git a/venv/Lib/site-packages/absl/logging/__init__.py b/venv/Lib/site-packages/absl/logging/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..532bd758d56e75778fca62437a6192c5a36531bc
--- /dev/null
+++ b/venv/Lib/site-packages/absl/logging/__init__.py
@@ -0,0 +1,1335 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Abseil Python logging module implemented on top of standard logging.
+
+Simple usage::
+
+ from absl import logging
+
+ logging.info('Interesting Stuff')
+ logging.info('Interesting Stuff with Arguments: %d', 42)
+
+ logging.set_verbosity(logging.INFO)
+ logging.log(logging.DEBUG, 'This will *not* be printed')
+ logging.set_verbosity(logging.DEBUG)
+ logging.log(logging.DEBUG, 'This will be printed')
+
+ logging.warning('Worrying Stuff')
+ logging.error('Alarming Stuff')
+ logging.fatal('AAAAHHHHH!!!!') # Process exits.
+
+Usage note: Do not pre-format the strings in your program code.
+Instead, let the logging module perform argument interpolation.
+This saves cycles because strings that don't need to be printed
+are never formatted. Note that this module does not attempt to
+interpolate arguments when no arguments are given. In other words::
+
+ logging.info('Interesting Stuff: %s')
+
+does not raise an exception because logging.info() has only one
+argument, the message string.
+
+"Lazy" evaluation for debugging
+-------------------------------
+
+If you do something like this::
+
+ logging.debug('Thing: %s', thing.ExpensiveOp())
+
+then the ExpensiveOp will be evaluated even if nothing
+is printed to the log. To avoid this, use the level_debug() function::
+
+ if logging.level_debug():
+ logging.debug('Thing: %s', thing.ExpensiveOp())
+
+Per file level logging is supported by logging.vlog() and
+logging.vlog_is_on(). For example::
+
+ if logging.vlog_is_on(2):
+ logging.vlog(2, very_expensive_debug_message())
+
+Notes on Unicode
+----------------
+
+The log output is encoded as UTF-8. Don't pass data in other encodings in
+bytes() instances -- instead pass unicode string instances when you need to
+(for both the format string and arguments).
+
+Note on critical and fatal:
+Standard logging module defines fatal as an alias to critical, but it's not
+documented, and it does NOT actually terminate the program.
+This module only defines fatal but not critical, and it DOES terminate the
+program.
+
+The differences in behavior are historical and unfortunate.
+"""
+
+import collections
+from collections.abc import Mapping
+import getpass
+import inspect
+import io
+import itertools
+import logging
+import os
+import socket
+import struct
+import sys
+import tempfile
+import threading
+import time
+import timeit
+import traceback
+import warnings
+
+from absl import flags
+from absl.logging import converter
+
+# pylint: disable=g-import-not-at-top
+try:
+ from typing import NoReturn
+except ImportError:
+ pass
+
+# pylint: enable=g-import-not-at-top
+
+FLAGS = flags.FLAGS
+
+
+# Logging levels.
+FATAL = converter.ABSL_FATAL
+ERROR = converter.ABSL_ERROR
+WARNING = converter.ABSL_WARNING
+WARN = converter.ABSL_WARNING # Deprecated name.
+INFO = converter.ABSL_INFO
+DEBUG = converter.ABSL_DEBUG
+
+# Regex to match/parse log line prefixes.
+ABSL_LOGGING_PREFIX_REGEX = (
+ r'^(?P[IWEF])'
+ r'(?P\d\d)(?P\d\d) '
+ r'(?P\d\d):(?P\d\d):(?P\d\d)'
+ r'\.(?P\d\d\d\d\d\d) +'
+ r'(?P-?\d+) '
+ r'(?P[a-zA-Z<][\w._<>-]+):(?P\d+)')
+
+
+# Mask to convert integer thread ids to unsigned quantities for logging purposes
+_THREAD_ID_MASK = 2 ** (struct.calcsize('L') * 8) - 1
+
+# Extra property set on the LogRecord created by ABSLLogger when its level is
+# CRITICAL/FATAL.
+_ABSL_LOG_FATAL = '_absl_log_fatal'
+# Extra prefix added to the log message when a non-absl logger logs a
+# CRITICAL/FATAL message.
+_CRITICAL_PREFIX = 'CRITICAL - '
+
+# Used by findCaller to skip callers from */logging/__init__.py.
+_LOGGING_FILE_PREFIX = os.path.join('logging', '__init__.')
+
+# The ABSL logger instance, initialized in _initialize().
+_absl_logger = None
+# The ABSL handler instance, initialized in _initialize().
+_absl_handler = None
+
+
+_CPP_NAME_TO_LEVELS = {
+ 'debug': '0', # Abseil C++ has no DEBUG level, mapping it to INFO here.
+ 'info': '0',
+ 'warning': '1',
+ 'warn': '1',
+ 'error': '2',
+ 'fatal': '3'
+}
+
+_CPP_LEVEL_TO_NAMES = {
+ '0': 'info',
+ '1': 'warning',
+ '2': 'error',
+ '3': 'fatal',
+}
+
+
+class _VerbosityFlag(flags.Flag):
+ """Flag class for -v/--verbosity."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ flags.IntegerParser(), flags.ArgumentSerializer(), *args, **kwargs
+ )
+
+ @property
+ def value(self):
+ return self._value
+
+ @value.setter
+ def value(self, v):
+ self._value = v
+ self._update_logging_levels()
+
+ def _update_logging_levels(self):
+ """Updates absl logging levels to the current verbosity.
+
+ Visibility: module-private
+ """
+ if not _absl_logger:
+ return
+
+ if self._value <= converter.ABSL_DEBUG:
+ standard_verbosity = converter.absl_to_standard(self._value)
+ else:
+ # --verbosity is set to higher than 1 for vlog.
+ standard_verbosity = logging.DEBUG - (self._value - 1)
+
+ # Also update root level when absl_handler is used.
+ if _absl_handler in logging.root.handlers:
+ # Make absl logger inherit from the root logger. absl logger might have
+ # a non-NOTSET value if logging.set_verbosity() is called at import time.
+ _absl_logger.setLevel(logging.NOTSET)
+ logging.root.setLevel(standard_verbosity)
+ else:
+ _absl_logger.setLevel(standard_verbosity)
+
+
+class _LoggerLevelsFlag(flags.Flag):
+ """Flag class for --logger_levels."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ _LoggerLevelsParser(), _LoggerLevelsSerializer(), *args, **kwargs
+ )
+
+ @property
+ def value(self):
+ # For lack of an immutable type, be defensive and return a copy.
+ # Modifications to the dict aren't supported and won't have any affect.
+ # While Py3 could use MappingProxyType, that isn't deepcopy friendly, so
+ # just return a copy.
+ return self._value.copy()
+
+ @value.setter
+ def value(self, v):
+ self._value = {} if v is None else v
+ self._update_logger_levels()
+
+ def _update_logger_levels(self):
+ # Visibility: module-private.
+ # This is called by absl.app.run() during initialization.
+ for name, level in self._value.items():
+ logging.getLogger(name).setLevel(level)
+
+
+class _LoggerLevelsParser(flags.ArgumentParser):
+ """Parser for --logger_levels flag."""
+
+ def parse(self, value):
+ if isinstance(value, Mapping):
+ return value
+
+ pairs = [pair.strip() for pair in value.split(',') if pair.strip()]
+
+ # Preserve the order so that serialization is deterministic.
+ levels = collections.OrderedDict()
+ for name_level in pairs:
+ name, level = name_level.split(':', 1)
+ name = name.strip()
+ level = level.strip()
+ levels[name] = level
+ return levels
+
+
+class _LoggerLevelsSerializer:
+ """Serializer for --logger_levels flag."""
+
+ def serialize(self, value):
+ if isinstance(value, str):
+ return value
+ return ','.join(f'{name}:{level}' for name, level in value.items())
+
+
+class _StderrthresholdFlag(flags.Flag):
+ """Flag class for --stderrthreshold."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(
+ flags.ArgumentParser(), flags.ArgumentSerializer(), *args, **kwargs
+ )
+
+ @property
+ def value(self):
+ return self._value
+
+ @value.setter
+ def value(self, v):
+ if v in _CPP_LEVEL_TO_NAMES:
+ # --stderrthreshold also accepts numeric strings whose values are
+ # Abseil C++ log levels.
+ cpp_value = int(v)
+ v = _CPP_LEVEL_TO_NAMES[v] # Normalize to strings.
+ elif v.lower() in _CPP_NAME_TO_LEVELS:
+ v = v.lower()
+ if v == 'warn':
+ v = 'warning' # Use 'warning' as the canonical name.
+ cpp_value = int(_CPP_NAME_TO_LEVELS[v])
+ else:
+ raise ValueError(
+ '--stderrthreshold must be one of (case-insensitive) '
+ "'debug', 'info', 'warning', 'error', 'fatal', "
+ "or '0', '1', '2', '3', not '%s'" % v)
+
+ self._value = v
+
+
+LOGTOSTDERR = flags.DEFINE_boolean(
+ 'logtostderr',
+ False,
+ 'Should only log to stderr?',
+ allow_override_cpp=True,
+)
+ALSOLOGTOSTDERR = flags.DEFINE_boolean(
+ 'alsologtostderr',
+ False,
+ 'also log to stderr?',
+ allow_override_cpp=True,
+)
+LOG_DIR = flags.DEFINE_string(
+ 'log_dir',
+ os.getenv('TEST_TMPDIR', ''),
+ 'directory to write logfiles into',
+ allow_override_cpp=True,
+)
+VERBOSITY = flags.DEFINE_flag(
+ _VerbosityFlag(
+ 'verbosity',
+ -1,
+ (
+ 'Logging verbosity level. Messages logged at this level or lower'
+ ' will be included. Set to 1 for debug logging. If the flag was not'
+ ' set or supplied, the value will be changed from the default of -1'
+ ' (warning) to 0 (info) after flags are parsed.'
+ ),
+ short_name='v',
+ allow_hide_cpp=True,
+ )
+)
+LOGGER_LEVELS = flags.DEFINE_flag(
+ _LoggerLevelsFlag(
+ 'logger_levels',
+ {},
+ (
+ 'Specify log level of loggers. The format is a CSV list of '
+ '`name:level`. Where `name` is the logger name used with '
+ '`logging.getLogger()`, and `level` is a level name (INFO, DEBUG, '
+ 'etc). e.g. `myapp.foo:INFO,other.logger:DEBUG`'
+ ),
+ )
+)
+STDERRTHRESHOLD = flags.DEFINE_flag(
+ _StderrthresholdFlag(
+ 'stderrthreshold',
+ 'fatal',
+ (
+ 'log messages at this level, or more severe, to stderr in '
+ 'addition to the logfile. Possible values are '
+ "'debug', 'info', 'warning', 'error', and 'fatal'. "
+ 'Obsoletes --alsologtostderr. Using --alsologtostderr '
+ 'cancels the effect of this flag. Please also note that '
+ 'this flag is subject to --verbosity and requires logfile '
+ 'not be stderr.'
+ ),
+ allow_hide_cpp=True,
+ )
+)
+SHOWPREFIXFORINFO = flags.DEFINE_boolean(
+ 'showprefixforinfo',
+ True,
+ (
+ 'If False, do not prepend prefix to info messages '
+ "when it's logged to stderr, "
+ '--verbosity is set to INFO level, '
+ 'and python logging is used.'
+ ),
+)
+
+
+def get_verbosity():
+ """Returns the logging verbosity."""
+ return FLAGS['verbosity'].value
+
+
+def set_verbosity(v):
+ """Sets the logging verbosity.
+
+ Causes all messages of level <= v to be logged,
+ and all messages of level > v to be silently discarded.
+
+ Args:
+ v: int|str, the verbosity level as an integer or string. Legal string values
+ are those that can be coerced to an integer as well as case-insensitive
+ 'debug', 'info', 'warning', 'error', and 'fatal'.
+ """
+ try:
+ new_level = int(v)
+ except ValueError:
+ new_level = converter.ABSL_NAMES[v.upper()]
+ FLAGS.verbosity = new_level
+
+
+def set_stderrthreshold(s):
+ """Sets the stderr threshold to the value passed in.
+
+ Args:
+ s: str|int, valid strings values are case-insensitive 'debug',
+ 'info', 'warning', 'error', and 'fatal'; valid integer values are
+ logging.DEBUG|INFO|WARNING|ERROR|FATAL.
+
+ Raises:
+ ValueError: Raised when s is an invalid value.
+ """
+ if s in converter.ABSL_LEVELS:
+ FLAGS.stderrthreshold = converter.ABSL_LEVELS[s]
+ elif isinstance(s, str) and s.upper() in converter.ABSL_NAMES:
+ FLAGS.stderrthreshold = s
+ else:
+ raise ValueError(
+ 'set_stderrthreshold only accepts integer absl logging level '
+ 'from -3 to 1, or case-insensitive string values '
+ "'debug', 'info', 'warning', 'error', and 'fatal'. "
+ 'But found "{}" ({}).'.format(s, type(s)))
+
+
+def fatal(msg, *args, **kwargs):
+ # type: (Any, Any, Any) -> NoReturn
+ """Logs a fatal message."""
+ log(FATAL, msg, *args, **kwargs)
+
+
+def error(msg, *args, **kwargs):
+ """Logs an error message."""
+ log(ERROR, msg, *args, **kwargs)
+
+
+def warning(msg, *args, **kwargs):
+ """Logs a warning message."""
+ log(WARNING, msg, *args, **kwargs)
+
+
+def warn(msg, *args, **kwargs):
+ """Deprecated, use 'warning' instead."""
+ warnings.warn("The 'warn' function is deprecated, use 'warning' instead",
+ DeprecationWarning, 2)
+ log(WARNING, msg, *args, **kwargs)
+
+
+def info(msg, *args, **kwargs):
+ """Logs an info message."""
+ log(INFO, msg, *args, **kwargs)
+
+
+def debug(msg, *args, **kwargs):
+ """Logs a debug message."""
+ log(DEBUG, msg, *args, **kwargs)
+
+
+def exception(msg, *args, exc_info=True, **kwargs):
+ """Logs an exception, with traceback and message."""
+ error(msg, *args, exc_info=exc_info, **kwargs)
+
+
+def _fast_stack_trace():
+ """A fast stack trace that gets us the minimal information we need.
+
+ Compared to using `get_absl_logger().findCaller(stack_info=True)`, this
+ function is ~100x faster.
+
+ Returns:
+ A tuple of tuples of (filename, line_number, last_instruction_offset).
+ """
+ cur_stack = inspect.currentframe()
+ if cur_stack is None or cur_stack.f_back is None:
+ return tuple()
+ # We drop the first frame, which is this function itself.
+ cur_stack = cur_stack.f_back
+ call_stack = []
+ while cur_stack.f_back:
+ cur_stack = cur_stack.f_back
+ call_stack.append(
+ (cur_stack.f_code.co_filename, cur_stack.f_lineno, cur_stack.f_lasti)
+ )
+ return tuple(call_stack)
+
+
+# Counter to keep track of number of log entries per token.
+_log_counter_per_token = {}
+
+
+def _get_next_log_count_per_token(token):
+ """Wrapper for _log_counter_per_token. Thread-safe.
+
+ Args:
+ token: The token for which to look up the count.
+
+ Returns:
+ The number of times this function has been called with
+ *token* as an argument (starting at 0).
+ """
+ # Can't use a defaultdict because defaultdict isn't atomic, whereas
+ # setdefault is.
+ return next(_log_counter_per_token.setdefault(token, itertools.count()))
+
+
+def log_every_n(level, msg, n, *args, use_call_stack=False, **kwargs):
+ """Logs ``msg % args`` at level 'level' once per 'n' times.
+
+ Logs the 1st call, (N+1)st call, (2N+1)st call, etc.
+ Not threadsafe.
+
+ Args:
+ level: int, the absl logging level at which to log.
+ msg: str, the message to be logged.
+ n: int, the number of times this should be called before it is logged.
+ *args: The args to be substituted into the msg.
+ use_call_stack: bool, whether to include the call stack when counting the
+ number of times the message is logged.
+ **kwargs: May contain exc_info to add exception traceback to message.
+ """
+ caller_info = get_absl_logger().findCaller()
+ if use_call_stack:
+ # To reduce storage costs, we hash the call stack.
+ caller_info = (*caller_info[0:3], hash(_fast_stack_trace()))
+ count = _get_next_log_count_per_token(caller_info)
+ log_if(level, msg, not (count % n), *args, **kwargs)
+
+
+# Keeps track of the last log time of the given token.
+# Note: must be a dict since set/get is atomic in CPython.
+# Note: entries are never released as their number is expected to be low.
+_log_timer_per_token = {}
+
+
+def _seconds_have_elapsed(token, num_seconds):
+ """Tests if 'num_seconds' have passed since 'token' was requested.
+
+ Not strictly thread-safe - may log with the wrong frequency if called
+ concurrently from multiple threads. Accuracy depends on resolution of
+ 'timeit.default_timer()'.
+
+ Always returns True on the first call for a given 'token'.
+
+ Args:
+ token: The token for which to look up the count.
+ num_seconds: The number of seconds to test for.
+
+ Returns:
+ Whether it has been >= 'num_seconds' since 'token' was last requested.
+ """
+ now = timeit.default_timer()
+ then = _log_timer_per_token.get(token, None)
+ if then is None or (now - then) >= num_seconds:
+ _log_timer_per_token[token] = now
+ return True
+ else:
+ return False
+
+
+def log_every_n_seconds(
+ level, msg, n_seconds, *args, use_call_stack=False, **kwargs
+):
+ """Logs ``msg % args`` at level ``level`` iff ``n_seconds`` elapsed since last call.
+
+ Logs the first call, logs subsequent calls if 'n' seconds have elapsed since
+ the last logging call from the same call site (file + line). Not thread-safe.
+
+ Args:
+ level: int, the absl logging level at which to log.
+ msg: str, the message to be logged.
+ n_seconds: float or int, seconds which should elapse before logging again.
+ *args: The args to be substituted into the msg.
+ use_call_stack: bool, whether to include the call stack when counting the
+ number of times the message is logged.
+ **kwargs: May contain exc_info to add exception traceback to message.
+ """
+ caller_info = get_absl_logger().findCaller()
+ if use_call_stack:
+ # To reduce storage costs, we hash the call stack.
+ caller_info = (*caller_info[0:3], hash(_fast_stack_trace()))
+ should_log = _seconds_have_elapsed(caller_info, n_seconds)
+ log_if(level, msg, should_log, *args, **kwargs)
+
+
+def log_first_n(level, msg, n, *args, use_call_stack=False, **kwargs):
+ """Logs ``msg % args`` at level ``level`` only first ``n`` times.
+
+ Not threadsafe.
+
+ Args:
+ level: int, the absl logging level at which to log.
+ msg: str, the message to be logged.
+ n: int, the maximal number of times the message is logged.
+ *args: The args to be substituted into the msg.
+ use_call_stack: bool, whether to include the call stack when counting the
+ number of times the message is logged.
+ **kwargs: May contain exc_info to add exception traceback to message.
+ """
+ caller_info = get_absl_logger().findCaller()
+ if use_call_stack:
+ # To reduce storage costs, we hash the call stack.
+ caller_info = (*caller_info[0:3], hash(_fast_stack_trace()))
+ count = _get_next_log_count_per_token(caller_info)
+ log_if(level, msg, count < n, *args, **kwargs)
+
+
+def log_if(level, msg, condition, *args, **kwargs):
+ """Logs ``msg % args`` at level ``level`` only if condition is fulfilled."""
+ if condition:
+ log(level, msg, *args, **kwargs)
+
+
+def log(level, msg, *args, **kwargs):
+ """Logs ``msg % args`` at absl logging level ``level``.
+
+ If no args are given just print msg, ignoring any interpolation specifiers.
+
+ Args:
+ level: int, the absl logging level at which to log the message
+ (logging.DEBUG|INFO|WARNING|ERROR|FATAL). While some C++ verbose logging
+ level constants are also supported, callers should prefer explicit
+ logging.vlog() calls for such purpose.
+
+ msg: str, the message to be logged.
+ *args: The args to be substituted into the msg.
+ **kwargs: May contain exc_info to add exception traceback to message.
+ """
+ if level > converter.ABSL_DEBUG:
+ # Even though this function supports level that is greater than 1, users
+ # should use logging.vlog instead for such cases.
+ # Treat this as vlog, 1 is equivalent to DEBUG.
+ standard_level = converter.STANDARD_DEBUG - (level - 1)
+ else:
+ if level < converter.ABSL_FATAL:
+ level = converter.ABSL_FATAL
+ standard_level = converter.absl_to_standard(level)
+
+ # Match standard logging's behavior. Before use_absl_handler() and
+ # logging is configured, there is no handler attached on _absl_logger nor
+ # logging.root. So logs go no where.
+ if not logging.root.handlers:
+ logging.basicConfig()
+
+ _absl_logger.log(standard_level, msg, *args, **kwargs)
+
+
+def vlog(level, msg, *args, **kwargs):
+ """Log ``msg % args`` at C++ vlog level ``level``.
+
+ Args:
+ level: int, the C++ verbose logging level at which to log the message,
+ e.g. 1, 2, 3, 4... While absl level constants are also supported,
+ callers should prefer logging.log|debug|info|... calls for such purpose.
+ msg: str, the message to be logged.
+ *args: The args to be substituted into the msg.
+ **kwargs: May contain exc_info to add exception traceback to message.
+ """
+ log(level, msg, *args, **kwargs)
+
+
+def vlog_is_on(level):
+ """Checks if vlog is enabled for the given level in caller's source file.
+
+ Args:
+ level: int, the C++ verbose logging level at which to log the message,
+ e.g. 1, 2, 3, 4... While absl level constants are also supported,
+ callers should prefer level_debug|level_info|... calls for
+ checking those.
+
+ Returns:
+ True if logging is turned on for that level.
+ """
+
+ if level > converter.ABSL_DEBUG:
+ # Even though this function supports level that is greater than 1, users
+ # should use logging.vlog instead for such cases.
+ # Treat this as vlog, 1 is equivalent to DEBUG.
+ standard_level = converter.STANDARD_DEBUG - (level - 1)
+ else:
+ if level < converter.ABSL_FATAL:
+ level = converter.ABSL_FATAL
+ standard_level = converter.absl_to_standard(level)
+ return _absl_logger.isEnabledFor(standard_level)
+
+
+def flush():
+ """Flushes all log files."""
+ get_absl_handler().flush()
+
+
+def level_debug():
+ """Returns True if debug logging is turned on."""
+ return get_verbosity() >= DEBUG
+
+
+def level_info():
+ """Returns True if info logging is turned on."""
+ return get_verbosity() >= INFO
+
+
+def level_warning():
+ """Returns True if warning logging is turned on."""
+ return get_verbosity() >= WARNING
+
+
+level_warn = level_warning # Deprecated function.
+
+
+def level_error():
+ """Returns True if error logging is turned on."""
+ return get_verbosity() >= ERROR
+
+
+def get_log_file_name(level=INFO):
+ """Returns the name of the log file.
+
+ For Python logging, only one file is used and level is ignored. And it returns
+ empty string if it logs to stderr/stdout or the log stream has no `name`
+ attribute.
+
+ Args:
+ level: int, the absl.logging level.
+
+ Raises:
+ ValueError: Raised when `level` has an invalid value.
+ """
+ if level not in converter.ABSL_LEVELS:
+ raise ValueError(f'Invalid absl.logging level {level}')
+ stream = get_absl_handler().python_handler.stream
+ if (stream == sys.stderr or stream == sys.stdout or
+ not hasattr(stream, 'name')):
+ return ''
+ else:
+ return stream.name
+
+
+def find_log_dir_and_names(program_name=None, log_dir=None):
+ """Computes the directory and filename prefix for log file.
+
+ Args:
+ program_name: str|None, the filename part of the path to the program that is
+ running without its extension. e.g: if your program is called
+ ``usr/bin/foobar.py`` this method should probably be called with
+ ``program_name='foobar`` However, this is just a convention, you can pass
+ in any string you want, and it will be used as part of the log filename.
+ If you don't pass in anything, the default behavior is as described in the
+ example. In python standard logging mode, the program_name will be
+ prepended with ``py_`` if it is the ``program_name`` argument is omitted.
+ log_dir: str|None, the desired log directory.
+
+ Returns:
+ (log_dir, file_prefix, symlink_prefix)
+
+ Raises:
+ FileNotFoundError: raised when it cannot find a log directory.
+ """
+ if not program_name:
+ # Strip the extension (foobar.par becomes foobar, and
+ # fubar.py becomes fubar). We do this so that the log
+ # file names are similar to C++ log file names.
+ program_name = os.path.splitext(os.path.basename(sys.argv[0]))[0]
+
+ # Prepend py_ to files so that python code gets a unique file, and
+ # so that C++ libraries do not try to write to the same log files as us.
+ program_name = 'py_%s' % program_name
+
+ actual_log_dir = find_log_dir(log_dir=log_dir)
+
+ try:
+ username = getpass.getuser()
+ except KeyError:
+ # This can happen, e.g. when running under docker w/o passwd file.
+ if hasattr(os, 'getuid'):
+ # Windows doesn't have os.getuid
+ username = str(os.getuid())
+ else:
+ username = 'unknown'
+ hostname = socket.gethostname()
+ file_prefix = '%s.%s.%s.log' % (program_name, hostname, username)
+
+ return actual_log_dir, file_prefix, program_name
+
+
+def find_log_dir(log_dir=None):
+ """Returns the most suitable directory to put log files into.
+
+ Args:
+ log_dir: str|None, if specified, the logfile(s) will be created in that
+ directory. Otherwise if the --log_dir command-line flag is provided, the
+ logfile will be created in that directory. Otherwise the logfile will be
+ created in a standard location.
+
+ Raises:
+ FileNotFoundError: raised when it cannot find a log directory.
+ """
+ # Get a list of possible log dirs (will try to use them in order).
+ # NOTE: Google's internal implementation has a special handling for Google
+ # machines, which uses a list of directories. Hence the following uses `dirs`
+ # instead of a single directory.
+ if log_dir:
+ # log_dir was explicitly specified as an arg, so use it and it alone.
+ dirs = [log_dir]
+ elif FLAGS['log_dir'].value:
+ # log_dir flag was provided, so use it and it alone (this mimics the
+ # behavior of the same flag in logging.cc).
+ dirs = [FLAGS['log_dir'].value]
+ else:
+ dirs = [tempfile.gettempdir()]
+
+ # Find the first usable log dir.
+ for d in dirs:
+ if os.path.isdir(d) and os.access(d, os.W_OK):
+ return d
+ raise FileNotFoundError(
+ "Can't find a writable directory for logs, tried %s" % dirs)
+
+
+def get_absl_log_prefix(record):
+ """Returns the absl log prefix for the log record.
+
+ Args:
+ record: logging.LogRecord, the record to get prefix for.
+ """
+ created_tuple = time.localtime(record.created)
+ created_microsecond = int(record.created % 1.0 * 1e6)
+
+ critical_prefix = ''
+ level = record.levelno
+ if _is_non_absl_fatal_record(record):
+ # When the level is FATAL, but not logged from absl, lower the level so
+ # it's treated as ERROR.
+ level = logging.ERROR
+ critical_prefix = _CRITICAL_PREFIX
+ severity = converter.get_initial_for_level(level)
+
+ return '%c%02d%02d %02d:%02d:%02d.%06d %5d %s:%d] %s' % (
+ severity,
+ created_tuple.tm_mon,
+ created_tuple.tm_mday,
+ created_tuple.tm_hour,
+ created_tuple.tm_min,
+ created_tuple.tm_sec,
+ created_microsecond,
+ _get_thread_id(),
+ record.filename,
+ record.lineno,
+ critical_prefix)
+
+
+def skip_log_prefix(func):
+ """Skips reporting the prefix of a given function or name by :class:`~absl.logging.ABSLLogger`.
+
+ This is a convenience wrapper function / decorator for
+ :meth:`~absl.logging.ABSLLogger.register_frame_to_skip`.
+
+ If a callable function is provided, only that function will be skipped.
+ If a function name is provided, all functions with the same name in the
+ file that this is called in will be skipped.
+
+ This can be used as a decorator of the intended function to be skipped.
+
+ Args:
+ func: Callable function or its name as a string.
+
+ Returns:
+ func (the input, unchanged).
+
+ Raises:
+ ValueError: The input is callable but does not have a function code object.
+ TypeError: The input is neither callable nor a string.
+ """
+ if callable(func):
+ func_code = getattr(func, '__code__', None)
+ if func_code is None:
+ raise ValueError('Input callable does not have a function code object.')
+ file_name = func_code.co_filename
+ func_name = func_code.co_name
+ func_lineno = func_code.co_firstlineno
+ elif isinstance(func, str):
+ file_name = get_absl_logger().findCaller()[0]
+ func_name = func
+ func_lineno = None
+ else:
+ raise TypeError('Input is neither callable nor a string.')
+ ABSLLogger.register_frame_to_skip(file_name, func_name, func_lineno)
+ return func
+
+
+def _is_non_absl_fatal_record(log_record):
+ return (log_record.levelno >= logging.FATAL and
+ not log_record.__dict__.get(_ABSL_LOG_FATAL, False))
+
+
+def _is_absl_fatal_record(log_record):
+ return (log_record.levelno >= logging.FATAL and
+ log_record.__dict__.get(_ABSL_LOG_FATAL, False))
+
+
+# Indicates if we still need to warn about pre-init logs going to stderr.
+_warn_preinit_stderr = True
+
+
+class PythonHandler(logging.StreamHandler):
+ """The handler class used by Abseil Python logging implementation."""
+
+ def __init__(self, stream=None, formatter=None):
+ super().__init__(stream)
+ self.setFormatter(formatter or PythonFormatter())
+
+ def start_logging_to_file(self, program_name=None, log_dir=None):
+ """Starts logging messages to files instead of standard error."""
+ FLAGS.logtostderr = False
+
+ actual_log_dir, file_prefix, symlink_prefix = find_log_dir_and_names(
+ program_name=program_name, log_dir=log_dir)
+
+ basename = '%s.INFO.%s.%d' % (
+ file_prefix,
+ time.strftime('%Y%m%d-%H%M%S', time.localtime(time.time())),
+ os.getpid())
+ filename = os.path.join(actual_log_dir, basename)
+
+ self.stream = open(filename, 'a', encoding='utf-8')
+
+ # os.symlink is not available on Windows Python 2.
+ if getattr(os, 'symlink', None):
+ # Create a symlink to the log file with a canonical name.
+ symlink = os.path.join(actual_log_dir, symlink_prefix + '.INFO')
+ try:
+ if os.path.islink(symlink):
+ os.unlink(symlink)
+ os.symlink(os.path.basename(filename), symlink)
+ except OSError:
+ # If it fails, we're sad but it's no error. Commonly, this
+ # fails because the symlink was created by another user and so
+ # we can't modify it
+ pass
+
+ def use_absl_log_file(self, program_name=None, log_dir=None):
+ """Conditionally logs to files, based on --logtostderr."""
+ if FLAGS['logtostderr'].value:
+ self.stream = sys.stderr
+ else:
+ self.start_logging_to_file(program_name=program_name, log_dir=log_dir)
+
+ def flush(self):
+ """Flushes all log files."""
+ self.acquire()
+ try:
+ if self.stream and hasattr(self.stream, 'flush'):
+ self.stream.flush()
+ except (OSError, ValueError):
+ # A ValueError is thrown if we try to flush a closed file.
+ pass
+ finally:
+ self.release()
+
+ def _log_to_stderr(self, record):
+ """Emits the record to stderr.
+
+ This temporarily sets the handler stream to stderr, calls
+ StreamHandler.emit, then reverts the stream back.
+
+ Args:
+ record: logging.LogRecord, the record to log.
+ """
+ # emit() is protected by a lock in logging.Handler, so we don't need to
+ # protect here again.
+ old_stream = self.stream
+ self.stream = sys.stderr
+ try:
+ super().emit(record)
+ finally:
+ self.stream = old_stream
+
+ def emit(self, record):
+ """Prints a record out to some streams.
+
+ 1. If ``FLAGS.logtostderr`` is set, it will print to ``sys.stderr`` ONLY.
+ 2. If ``FLAGS.alsologtostderr`` is set, it will print to ``sys.stderr``.
+ 3. If ``FLAGS.logtostderr`` is not set, it will log to the stream
+ associated with the current thread.
+
+ Args:
+ record: :class:`logging.LogRecord`, the record to emit.
+ """
+ # People occasionally call logging functions at import time before
+ # our flags may have even been defined yet, let alone even parsed, as we
+ # rely on the C++ side to define some flags for us and app init to
+ # deal with parsing. Match the C++ library behavior of notify and emit
+ # such messages to stderr. It encourages people to clean-up and does
+ # not hide the message.
+ level = record.levelno
+ if not FLAGS.is_parsed(): # Also implies "before flag has been defined".
+ global _warn_preinit_stderr
+ if _warn_preinit_stderr:
+ sys.stderr.write(
+ 'WARNING: Logging before flag parsing goes to stderr.\n')
+ _warn_preinit_stderr = False
+ self._log_to_stderr(record)
+ elif FLAGS['logtostderr'].value:
+ self._log_to_stderr(record)
+ else:
+ super().emit(record)
+ stderr_threshold = converter.string_to_standard(
+ FLAGS['stderrthreshold'].value)
+ if ((FLAGS['alsologtostderr'].value or level >= stderr_threshold) and
+ self.stream != sys.stderr):
+ self._log_to_stderr(record)
+ # Die when the record is created from ABSLLogger and level is FATAL.
+ if _is_absl_fatal_record(record):
+ self.flush() # Flush the log before dying.
+
+ # In threaded python, sys.exit() from a non-main thread only
+ # exits the thread in question.
+ os.abort()
+
+ def close(self):
+ """Closes the stream to which we are writing."""
+ self.acquire()
+ try:
+ self.flush()
+ try:
+ # Do not close the stream if it's sys.stderr|stdout. They may be
+ # redirected or overridden to files, which should be managed by users
+ # explicitly.
+ user_managed = sys.stderr, sys.stdout, sys.__stderr__, sys.__stdout__
+ if self.stream not in user_managed and (
+ not hasattr(self.stream, 'isatty') or not self.stream.isatty()):
+ self.stream.close()
+ except ValueError:
+ # A ValueError is thrown if we try to run isatty() on a closed file.
+ pass
+ super().close()
+ finally:
+ self.release()
+
+
+class ABSLHandler(logging.Handler):
+ """Abseil Python logging module's log handler."""
+
+ def __init__(self, python_logging_formatter):
+ super().__init__()
+
+ self._python_handler = PythonHandler(formatter=python_logging_formatter)
+ self.activate_python_handler()
+
+ def format(self, record):
+ return self._current_handler.format(record)
+
+ def setFormatter(self, fmt):
+ self._current_handler.setFormatter(fmt)
+
+ def emit(self, record):
+ self._current_handler.emit(record)
+
+ def flush(self):
+ self._current_handler.flush()
+
+ def close(self):
+ super().close()
+ self._current_handler.close()
+
+ def handle(self, record):
+ rv = self.filter(record)
+ if rv:
+ return self._current_handler.handle(record)
+ return rv
+
+ @property
+ def python_handler(self):
+ return self._python_handler
+
+ def activate_python_handler(self):
+ """Uses the Python logging handler as the current logging handler."""
+ self._current_handler = self._python_handler
+
+ def use_absl_log_file(self, program_name=None, log_dir=None):
+ self._current_handler.use_absl_log_file(program_name, log_dir)
+
+ def start_logging_to_file(self, program_name=None, log_dir=None):
+ self._current_handler.start_logging_to_file(program_name, log_dir)
+
+
+class PythonFormatter(logging.Formatter):
+ """Formatter class used by :class:`~absl.logging.PythonHandler`."""
+
+ def format(self, record):
+ """Appends the message from the record to the results of the prefix.
+
+ Args:
+ record: logging.LogRecord, the record to be formatted.
+
+ Returns:
+ The formatted string representing the record.
+ """
+ if (not FLAGS['showprefixforinfo'].value and
+ FLAGS['verbosity'].value == converter.ABSL_INFO and
+ record.levelno == logging.INFO and
+ _absl_handler.python_handler.stream == sys.stderr):
+ prefix = ''
+ else:
+ prefix = get_absl_log_prefix(record)
+ return prefix + super().format(record)
+
+
+class ABSLLogger(logging.getLoggerClass()):
+ """A logger that will create LogRecords while skipping some stack frames.
+
+ This class maintains an internal list of filenames and method names
+ for use when determining who called the currently executing stack
+ frame. Any method names from specific source files are skipped when
+ walking backwards through the stack.
+
+ Client code should use the register_frame_to_skip method to let the
+ ABSLLogger know which method from which file should be
+ excluded from the walk backwards through the stack.
+ """
+ _frames_to_skip = set()
+
+ def findCaller(self, stack_info=False, stacklevel=1):
+ """Finds the frame of the calling method on the stack.
+
+ This method skips any frames registered with the
+ ABSLLogger and any methods from this file, and whatever
+ method is currently being used to generate the prefix for the log
+ line. Then it returns the file name, line number, and method name
+ of the calling method. An optional fourth item may be returned,
+ callers who only need things from the first three are advised to
+ always slice or index the result rather than using direct unpacking
+ assignment.
+
+ Args:
+ stack_info: bool, when True, include the stack trace as a fourth item
+ returned. On Python 3 there are always four items returned - the fourth
+ will be None when this is False. On Python 2 the stdlib base class API
+ only returns three items. We do the same when this new parameter is
+ unspecified or False for compatibility.
+ stacklevel: int, if greater than 1, that number of frames will be skipped.
+
+ Returns:
+ (filename, lineno, methodname[, sinfo]) of the calling method.
+ """
+ f_to_skip = ABSLLogger._frames_to_skip
+ # Use sys._getframe(2) instead of logging.currentframe(), it's slightly
+ # faster because there is one less frame to traverse.
+ frame = sys._getframe(2) # pylint: disable=protected-access
+ frame_to_return = None
+
+ while frame:
+ code = frame.f_code
+ if (_LOGGING_FILE_PREFIX not in code.co_filename and
+ (code.co_filename, code.co_name,
+ code.co_firstlineno) not in f_to_skip and
+ (code.co_filename, code.co_name) not in f_to_skip):
+ frame_to_return = frame
+ stacklevel -= 1
+ if stacklevel <= 0:
+ break
+ frame = frame.f_back
+
+ if frame_to_return is not None:
+ sinfo = None
+ if stack_info:
+ out = io.StringIO()
+ out.write('Stack (most recent call last):\n')
+ traceback.print_stack(frame, file=out)
+ sinfo = out.getvalue().rstrip('\n')
+ return (
+ frame_to_return.f_code.co_filename,
+ frame_to_return.f_lineno,
+ frame_to_return.f_code.co_name,
+ sinfo,
+ )
+
+ return None
+
+ def critical(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``CRITICAL``."""
+ self.log(logging.CRITICAL, msg, *args, **kwargs)
+
+ def fatal(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``FATAL``."""
+ self.log(logging.FATAL, msg, *args, **kwargs)
+
+ def error(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``ERROR``."""
+ self.log(logging.ERROR, msg, *args, **kwargs)
+
+ def warn(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``WARN``."""
+ warnings.warn("The 'warn' method is deprecated, use 'warning' instead",
+ DeprecationWarning, 2)
+ self.log(logging.WARN, msg, *args, **kwargs)
+
+ def warning(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``WARNING``."""
+ self.log(logging.WARNING, msg, *args, **kwargs)
+
+ def info(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``INFO``."""
+ self.log(logging.INFO, msg, *args, **kwargs)
+
+ def debug(self, msg, *args, **kwargs):
+ """Logs ``msg % args`` with severity ``DEBUG``."""
+ self.log(logging.DEBUG, msg, *args, **kwargs)
+
+ def log(self, level, msg, *args, **kwargs):
+ """Logs a message at a certain level substituting in the supplied arguments.
+
+ This method behaves differently in python and c++ modes.
+
+ Args:
+ level: int, the standard logging level at which to log the message.
+ msg: str, the text of the message to log.
+ *args: The arguments to substitute in the message.
+ **kwargs: The keyword arguments to substitute in the message.
+ """
+ if level >= logging.FATAL:
+ # Add property to the LogRecord created by this logger.
+ # This will be used by the ABSLHandler to determine whether it should
+ # treat CRITICAL/FATAL logs as really FATAL.
+ extra = kwargs.setdefault('extra', {})
+ extra[_ABSL_LOG_FATAL] = True
+ super().log(level, msg, *args, **kwargs)
+
+ def handle(self, record):
+ """Calls handlers without checking ``Logger.disabled``.
+
+ Non-root loggers are set to disabled after setup with :func:`logging.config`
+ if it's not explicitly specified. Historically, absl logging will not be
+ disabled by that. To maintaining this behavior, this function skips
+ checking the ``Logger.disabled`` bit.
+
+ This logger can still be disabled by adding a filter that filters out
+ everything.
+
+ Args:
+ record: logging.LogRecord, the record to handle.
+ """
+ if self.filter(record):
+ self.callHandlers(record)
+
+ @classmethod
+ def register_frame_to_skip(cls, file_name, function_name, line_number=None):
+ """Registers a function name to skip when walking the stack.
+
+ The :class:`~absl.logging.ABSLLogger` sometimes skips method calls on the
+ stack to make the log messages meaningful in their appropriate context.
+ This method registers a function from a particular file as one
+ which should be skipped.
+
+ Args:
+ file_name: str, the name of the file that contains the function.
+ function_name: str, the name of the function to skip.
+ line_number: int, if provided, only the function with this starting line
+ number will be skipped. Otherwise, all functions with the same name
+ in the file will be skipped.
+ """
+ if line_number is not None:
+ cls._frames_to_skip.add((file_name, function_name, line_number))
+ else:
+ cls._frames_to_skip.add((file_name, function_name))
+
+
+def _get_thread_id():
+ """Gets id of current thread, suitable for logging as an unsigned quantity.
+
+ If pywrapbase is linked, returns GetTID() for the thread ID to be
+ consistent with C++ logging. Otherwise, returns the numeric thread id.
+ The quantities are made unsigned by masking with 2*sys.maxint + 1.
+
+ Returns:
+ Thread ID unique to this process (unsigned)
+ """
+ thread_id = threading.get_ident()
+ return thread_id & _THREAD_ID_MASK
+
+
+def get_absl_logger():
+ """Returns the absl logger instance."""
+ assert _absl_logger is not None
+ return _absl_logger
+
+
+def get_absl_handler():
+ """Returns the absl handler instance."""
+ assert _absl_handler is not None
+ return _absl_handler
+
+
+def use_python_logging(quiet=False):
+ """Uses the python implementation of the logging code.
+
+ Args:
+ quiet: No logging message about switching logging type.
+ """
+ get_absl_handler().activate_python_handler()
+ if not quiet:
+ info('Restoring pure python logging')
+
+
+_attempted_to_remove_stderr_stream_handlers = False
+
+
+def use_absl_handler():
+ """Uses the ABSL logging handler for logging.
+
+ This method is called in :func:`app.run()` so the absl handler
+ is used in absl apps.
+ """
+ global _attempted_to_remove_stderr_stream_handlers
+ if not _attempted_to_remove_stderr_stream_handlers:
+ # The absl handler logs to stderr by default. To prevent double logging to
+ # stderr, the following code tries its best to remove other handlers that
+ # emit to stderr. Those handlers are most commonly added when
+ # logging.info/debug is called before calling use_absl_handler().
+ handlers = [
+ h for h in logging.root.handlers
+ if isinstance(h, logging.StreamHandler) and h.stream == sys.stderr]
+ for h in handlers:
+ logging.root.removeHandler(h)
+ _attempted_to_remove_stderr_stream_handlers = True
+
+ absl_handler = get_absl_handler()
+ if absl_handler not in logging.root.handlers:
+ logging.root.addHandler(absl_handler)
+ FLAGS['verbosity']._update_logging_levels() # pylint: disable=protected-access
+ FLAGS['logger_levels']._update_logger_levels() # pylint: disable=protected-access
+
+
+def _initialize():
+ """Initializes loggers and handlers."""
+ global _absl_logger, _absl_handler
+
+ if _absl_logger:
+ return
+
+ original_logger_class = logging.getLoggerClass()
+ logging.setLoggerClass(ABSLLogger)
+ _absl_logger = logging.getLogger('absl')
+ logging.setLoggerClass(original_logger_class)
+
+ python_logging_formatter = PythonFormatter()
+ _absl_handler = ABSLHandler(python_logging_formatter)
+
+
+_initialize()
diff --git a/venv/Lib/site-packages/absl/logging/__init__.pyi b/venv/Lib/site-packages/absl/logging/__init__.pyi
new file mode 100644
index 0000000000000000000000000000000000000000..7e69ecb17177381accb5640fa580b7abb00183bf
--- /dev/null
+++ b/venv/Lib/site-packages/absl/logging/__init__.pyi
@@ -0,0 +1,278 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from collections.abc import Callable
+import logging
+from typing import Any, NoReturn, TypeVar
+
+from absl import flags
+
+# Logging levels.
+FATAL: int
+ERROR: int
+WARNING: int
+WARN: int # Deprecated name.
+INFO: int
+DEBUG: int
+
+ABSL_LOGGING_PREFIX_REGEX: str
+
+LOGTOSTDERR: flags.FlagHolder[bool]
+ALSOLOGTOSTDERR: flags.FlagHolder[bool]
+LOG_DIR: flags.FlagHolder[str]
+VERBOSITY: flags.FlagHolder[int]
+LOGGER_LEVELS: flags.FlagHolder[dict[str, str]]
+STDERRTHRESHOLD: flags.FlagHolder[str]
+SHOWPREFIXFORINFO: flags.FlagHolder[bool]
+
+def get_verbosity() -> int:
+ ...
+
+def set_verbosity(v: int | str) -> None:
+ ...
+
+def set_stderrthreshold(s: int | str) -> None:
+ ...
+
+# TODO(b/277607978): Provide actual args+kwargs shadowing stdlib's logging functions.
+def fatal(msg: Any, *args: Any, **kwargs: Any) -> NoReturn:
+ ...
+
+def error(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def warning(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def warn(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def info(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def debug(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def exception(msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def log_every_n(
+ level: int,
+ msg: Any,
+ n: int,
+ *args: Any,
+ use_call_stack: bool = ...,
+ **kwargs: Any,
+) -> None:
+ ...
+
+def log_every_n_seconds(
+ level: int,
+ msg: Any,
+ n_seconds: float,
+ *args: Any,
+ use_call_stack: bool = ...,
+ **kwargs: Any,
+) -> None:
+ ...
+
+def log_first_n(
+ level: int,
+ msg: Any,
+ n: int,
+ *args: Any,
+ use_call_stack: bool = ...,
+ **kwargs: Any,
+) -> None:
+ ...
+
+def log_if(level: int,
+ msg: Any,
+ condition: Any,
+ *args: Any,
+ **kwargs: Any,
+) -> None:
+ ...
+
+def log(level: int, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def vlog(level: int, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+def vlog_is_on(level: int) -> bool:
+ ...
+
+def flush() -> None:
+ ...
+
+def level_debug() -> bool:
+ ...
+
+def level_info() -> bool:
+ ...
+
+def level_warning() -> bool:
+ ...
+
+level_warn = level_warning # Deprecated function.
+
+def level_error() -> bool:
+ ...
+
+def get_log_file_name(level: int = ...) -> str:
+ ...
+
+def find_log_dir_and_names(
+ program_name: str | None = ..., log_dir: str | None = ...
+) -> tuple[str, str, str]:
+ ...
+
+def find_log_dir(log_dir: str | None = ...) -> str:
+ ...
+
+def get_absl_log_prefix(record: logging.LogRecord) -> str:
+ ...
+
+_SkipLogT = TypeVar('_SkipLogT', str, Callable[..., Any])
+
+def skip_log_prefix(func: _SkipLogT) -> _SkipLogT:
+ ...
+
+_StreamT = TypeVar('_StreamT')
+
+class PythonHandler(logging.StreamHandler[_StreamT]): # type: ignore[type-var]
+
+ def __init__(
+ self,
+ stream: _StreamT | None = ...,
+ formatter: logging.Formatter | None = ...,
+ ) -> None:
+ ...
+
+ def start_logging_to_file(
+ self, program_name: str | None = ..., log_dir: str | None = ...
+ ) -> None:
+ ...
+
+ def use_absl_log_file(
+ self, program_name: str | None = ..., log_dir: str | None = ...
+ ) -> None:
+ ...
+
+ def flush(self) -> None:
+ ...
+
+ def emit(self, record: logging.LogRecord) -> None:
+ ...
+
+ def close(self) -> None:
+ ...
+
+class ABSLHandler(logging.Handler):
+
+ def __init__(self, python_logging_formatter: PythonFormatter) -> None:
+ ...
+
+ def format(self, record: logging.LogRecord) -> str:
+ ...
+
+ def setFormatter(self, fmt) -> None:
+ ...
+
+ def emit(self, record: logging.LogRecord) -> None:
+ ...
+
+ def flush(self) -> None:
+ ...
+
+ def close(self) -> None:
+ ...
+
+ def handle(self, record: logging.LogRecord) -> bool:
+ ...
+
+ @property
+ def python_handler(self) -> PythonHandler:
+ ...
+
+ def activate_python_handler(self) -> None:
+ ...
+
+ def use_absl_log_file(
+ self, program_name: str | None = ..., log_dir: str | None = ...
+ ) -> None:
+ ...
+
+ def start_logging_to_file(self, program_name=None, log_dir=None) -> None:
+ ...
+
+class PythonFormatter(logging.Formatter):
+
+ def format(self, record: logging.LogRecord) -> str:
+ ...
+
+class ABSLLogger(logging.Logger):
+
+ def findCaller(
+ self, stack_info: bool = ..., stacklevel: int = ...
+ ) -> tuple[str, int, str, str | None]:
+ ...
+
+ def critical(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def fatal(self, msg: Any, *args: Any, **kwargs: Any) -> NoReturn: # type: ignore[override]
+ ...
+
+ def error(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def warn(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def warning(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def info(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def debug(self, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def log(self, level: int, msg: Any, *args: Any, **kwargs: Any) -> None:
+ ...
+
+ def handle(self, record: logging.LogRecord) -> None:
+ ...
+
+ @classmethod
+ def register_frame_to_skip(
+ cls, file_name: str, function_name: str, line_number: int | None = ...
+ ) -> None:
+ ...
+
+# NOTE: Returns None before _initialize called but shouldn't occur after import.
+def get_absl_logger() -> ABSLLogger:
+ ...
+
+# NOTE: Returns None before _initialize called but shouldn't occur after import.
+def get_absl_handler() -> ABSLHandler:
+ ...
+
+def use_python_logging(quiet: bool = ...) -> None:
+ ...
+
+def use_absl_handler() -> None:
+ ...
diff --git a/venv/Lib/site-packages/absl/logging/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/absl/logging/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..bcfa7f9594afbc81685f6206183ea80e1c8eb435
Binary files /dev/null and b/venv/Lib/site-packages/absl/logging/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/logging/__pycache__/converter.cpython-311.pyc b/venv/Lib/site-packages/absl/logging/__pycache__/converter.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..dfc6d82ededc60dc80c76927c4f1ca1faf8ef8c2
Binary files /dev/null and b/venv/Lib/site-packages/absl/logging/__pycache__/converter.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/logging/converter.py b/venv/Lib/site-packages/absl/logging/converter.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad3fcd5045edc665cc3118a9caa4f44fc1607be2
--- /dev/null
+++ b/venv/Lib/site-packages/absl/logging/converter.py
@@ -0,0 +1,214 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Module to convert log levels between Abseil Python, C++, and Python standard.
+
+This converter has to convert (best effort) between three different
+logging level schemes:
+
+ * **cpp**: The C++ logging level scheme used in Abseil C++.
+ * **absl**: The absl.logging level scheme used in Abseil Python.
+ * **standard**: The python standard library logging level scheme.
+
+Here is a handy ascii chart for easy mental mapping::
+
+ LEVEL | cpp | absl | standard |
+ ---------+-----+--------+----------+
+ DEBUG | 0 | 1 | 10 |
+ INFO | 0 | 0 | 20 |
+ WARNING | 1 | -1 | 30 |
+ ERROR | 2 | -2 | 40 |
+ CRITICAL | 3 | -3 | 50 |
+ FATAL | 3 | -3 | 50 |
+
+Note: standard logging ``CRITICAL`` is mapped to absl/cpp ``FATAL``.
+However, only ``CRITICAL`` logs from the absl logger (or absl.logging.fatal)
+will terminate the program. ``CRITICAL`` logs from non-absl loggers are treated
+as error logs with a message prefix ``"CRITICAL - "``.
+
+Converting from standard to absl or cpp is a lossy conversion.
+Converting back to standard will lose granularity. For this reason,
+users should always try to convert to standard, the richest
+representation, before manipulating the levels, and then only to cpp
+or absl if those level schemes are absolutely necessary.
+"""
+
+import logging
+
+STANDARD_CRITICAL = logging.CRITICAL
+STANDARD_ERROR = logging.ERROR
+STANDARD_WARNING = logging.WARNING
+STANDARD_INFO = logging.INFO
+STANDARD_DEBUG = logging.DEBUG
+
+# These levels are also used to define the constants
+# FATAL, ERROR, WARNING, INFO, and DEBUG in the
+# absl.logging module.
+ABSL_FATAL = -3
+ABSL_ERROR = -2
+ABSL_WARNING = -1
+ABSL_WARN = -1 # Deprecated name.
+ABSL_INFO = 0
+ABSL_DEBUG = 1
+
+ABSL_LEVELS = {ABSL_FATAL: 'FATAL',
+ ABSL_ERROR: 'ERROR',
+ ABSL_WARNING: 'WARNING',
+ ABSL_INFO: 'INFO',
+ ABSL_DEBUG: 'DEBUG'}
+
+# Inverts the ABSL_LEVELS dictionary
+ABSL_NAMES = {'FATAL': ABSL_FATAL,
+ 'ERROR': ABSL_ERROR,
+ 'WARNING': ABSL_WARNING,
+ 'WARN': ABSL_WARNING, # Deprecated name.
+ 'INFO': ABSL_INFO,
+ 'DEBUG': ABSL_DEBUG}
+
+ABSL_TO_STANDARD = {ABSL_FATAL: STANDARD_CRITICAL,
+ ABSL_ERROR: STANDARD_ERROR,
+ ABSL_WARNING: STANDARD_WARNING,
+ ABSL_INFO: STANDARD_INFO,
+ ABSL_DEBUG: STANDARD_DEBUG}
+
+# Inverts the ABSL_TO_STANDARD
+STANDARD_TO_ABSL = {v: k for (k, v) in ABSL_TO_STANDARD.items()}
+
+
+def get_initial_for_level(level):
+ """Gets the initial that should start the log line for the given level.
+
+ It returns:
+
+ * ``'I'`` when: ``level < STANDARD_WARNING``.
+ * ``'W'`` when: ``STANDARD_WARNING <= level < STANDARD_ERROR``.
+ * ``'E'`` when: ``STANDARD_ERROR <= level < STANDARD_CRITICAL``.
+ * ``'F'`` when: ``level >= STANDARD_CRITICAL``.
+
+ Args:
+ level: int, a Python standard logging level.
+
+ Returns:
+ The first initial as it would be logged by the C++ logging module.
+ """
+ if level < STANDARD_WARNING:
+ return 'I'
+ elif level < STANDARD_ERROR:
+ return 'W'
+ elif level < STANDARD_CRITICAL:
+ return 'E'
+ else:
+ return 'F'
+
+
+def absl_to_cpp(level):
+ """Converts an absl log level to a cpp log level.
+
+ Args:
+ level: int, an absl.logging level.
+
+ Raises:
+ TypeError: Raised when level is not an integer.
+
+ Returns:
+ The corresponding integer level for use in Abseil C++.
+ """
+ if not isinstance(level, int):
+ raise TypeError(f'Expect an int level, found {type(level)}')
+ if level >= 0:
+ # C++ log levels must be >= 0
+ return 0
+ else:
+ return -level
+
+
+def absl_to_standard(level):
+ """Converts an integer level from the absl value to the standard value.
+
+ Args:
+ level: int, an absl.logging level.
+
+ Raises:
+ TypeError: Raised when level is not an integer.
+
+ Returns:
+ The corresponding integer level for use in standard logging.
+ """
+ if not isinstance(level, int):
+ raise TypeError(f'Expect an int level, found {type(level)}')
+ if level < ABSL_FATAL:
+ level = ABSL_FATAL
+ if level <= ABSL_DEBUG:
+ return ABSL_TO_STANDARD[level]
+ # Maps to vlog levels.
+ return STANDARD_DEBUG - level + 1
+
+
+def string_to_standard(level):
+ """Converts a string level to standard logging level value.
+
+ Args:
+ level: str, case-insensitive ``'debug'``, ``'info'``, ``'warning'``,
+ ``'error'``, ``'fatal'``.
+
+ Returns:
+ The corresponding integer level for use in standard logging.
+ """
+ return absl_to_standard(ABSL_NAMES.get(level.upper()))
+
+
+def standard_to_absl(level):
+ """Converts an integer level from the standard value to the absl value.
+
+ Args:
+ level: int, a Python standard logging level.
+
+ Raises:
+ TypeError: Raised when level is not an integer.
+
+ Returns:
+ The corresponding integer level for use in absl logging.
+ """
+ if not isinstance(level, int):
+ raise TypeError(f'Expect an int level, found {type(level)}')
+ if level < 0:
+ level = 0
+ if level < STANDARD_DEBUG:
+ # Maps to vlog levels.
+ return STANDARD_DEBUG - level + 1
+ elif level < STANDARD_INFO:
+ return ABSL_DEBUG
+ elif level < STANDARD_WARNING:
+ return ABSL_INFO
+ elif level < STANDARD_ERROR:
+ return ABSL_WARNING
+ elif level < STANDARD_CRITICAL:
+ return ABSL_ERROR
+ else:
+ return ABSL_FATAL
+
+
+def standard_to_cpp(level):
+ """Converts an integer level from the standard value to the cpp value.
+
+ Args:
+ level: int, a Python standard logging level.
+
+ Raises:
+ TypeError: Raised when level is not an integer.
+
+ Returns:
+ The corresponding integer level for use in cpp logging.
+ """
+ return absl_to_cpp(standard_to_absl(level))
diff --git a/venv/Lib/site-packages/absl/py.typed b/venv/Lib/site-packages/absl/py.typed
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/venv/Lib/site-packages/absl/testing/__init__.py b/venv/Lib/site-packages/absl/testing/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..a3bd1cd51810385ca0e5e9fed3fb9a804febf27e
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/__init__.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/__init__.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..34b9c76939d659ad4ff466f8ce13e4d30f214eab
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/__init__.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..a7cff0bc3f9c7679df913c63ef46cbddf54862d9
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/_bazelize_command.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..75ac5eb18d5386c02b5fa9cef778b964e568561a
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/_pretty_print_reporter.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/absltest.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/absltest.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..de599e7bfe9a7784260ad52d45c86af81d31dda8
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/__pycache__/absltest.cpython-311.pyc
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d1c5fed7f42a6dfeb011b9c8e963690eb2862da7df63ef5434c1b884fe43242
+size 134812
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/flagsaver.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/flagsaver.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..b6023e30e2cc581b80dbfa690d8bc1033147b0cc
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/flagsaver.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/parameterized.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/parameterized.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..38e241805ab66b2dd6c3bcc265d9ec0a9df58f91
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/parameterized.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/__pycache__/xml_reporter.cpython-311.pyc b/venv/Lib/site-packages/absl/testing/__pycache__/xml_reporter.cpython-311.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..50cea44fc0482a2ad4e7dd9a1c52478b83c23bfd
Binary files /dev/null and b/venv/Lib/site-packages/absl/testing/__pycache__/xml_reporter.cpython-311.pyc differ
diff --git a/venv/Lib/site-packages/absl/testing/_bazelize_command.py b/venv/Lib/site-packages/absl/testing/_bazelize_command.py
new file mode 100644
index 0000000000000000000000000000000000000000..acde3d2bd674a148ab3ec2a50984219c42b58a34
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/_bazelize_command.py
@@ -0,0 +1,68 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Internal helper for running tests on Windows Bazel."""
+
+import os
+
+from absl import flags
+
+FLAGS = flags.FLAGS
+
+
+def get_executable_path(py_binary_name):
+ """Returns the executable path of a py_binary.
+
+ This returns the executable path of a py_binary that is in another Bazel
+ target's data dependencies.
+
+ On Linux/macOS, the path and __file__ has the same root directory.
+ On Windows, bazel builds an .exe file and we need to use the MANIFEST file
+ the location the actual binary.
+
+ Args:
+ py_binary_name: string, the name of a py_binary that is in another Bazel
+ target's data dependencies.
+
+ Raises:
+ RuntimeError: Raised when it cannot locate the executable path.
+ """
+
+ if os.name == 'nt':
+ py_binary_name += '.exe'
+ manifest_file = os.path.join(FLAGS.test_srcdir, 'MANIFEST')
+ workspace_name = os.environ['TEST_WORKSPACE']
+ manifest_entry = f'{workspace_name}/{py_binary_name}'
+ with open(manifest_file) as manifest_fd:
+ for line in manifest_fd:
+ tokens = line.strip().split(' ')
+ if len(tokens) != 2:
+ continue
+ if manifest_entry == tokens[0]:
+ return tokens[1]
+ raise RuntimeError(
+ 'Cannot locate executable path for {}, MANIFEST file: {}.'.format(
+ py_binary_name, manifest_file))
+ else:
+ # NOTE: __file__ may be .py or .pyc, depending on how the module was
+ # loaded and executed.
+ path = __file__
+
+ # Use the package name to find the root directory: every dot is
+ # a directory, plus one for ourselves.
+ for _ in range(__name__.count('.') + 1):
+ path = os.path.dirname(path)
+
+ root_directory = path
+ return os.path.join(root_directory, py_binary_name)
diff --git a/venv/Lib/site-packages/absl/testing/_pretty_print_reporter.py b/venv/Lib/site-packages/absl/testing/_pretty_print_reporter.py
new file mode 100644
index 0000000000000000000000000000000000000000..af2bedbc6558b53c0d4bbd54a71859138cddb0c5
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/_pretty_print_reporter.py
@@ -0,0 +1,92 @@
+# Copyright 2018 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""TestResult implementing default output for test execution status."""
+
+import unittest
+
+
+class TextTestResult(unittest.TextTestResult):
+ """TestResult class that provides the default text result formatting."""
+
+ def __init__(self, stream, descriptions, verbosity):
+ # Disable the verbose per-test output from the superclass, since it would
+ # conflict with our customized output.
+ super().__init__(stream, descriptions, 0)
+ self._per_test_output = verbosity > 0
+
+ def _print_status(self, tag, test, reason=None):
+ if self._per_test_output:
+ test_id = test.id()
+ if test_id.startswith('__main__.'):
+ test_id = test_id[len('__main__.'):]
+ if reason:
+ print('[%s] %s - %s' % (tag, test_id, reason), file=self.stream)
+ else:
+ print('[%s] %s' % (tag, test_id), file=self.stream)
+ self.stream.flush()
+
+ def startTest(self, test):
+ super().startTest(test)
+ self._print_status(' RUN ', test)
+
+ def addSuccess(self, test):
+ super().addSuccess(test)
+ self._print_status(' OK ', test)
+
+ def addError(self, test, err):
+ super().addError(test, err)
+ self._print_status(' FAILED ', test)
+
+ def addFailure(self, test, err):
+ super().addFailure(test, err)
+ self._print_status(' FAILED ', test)
+
+ def addSkip(self, test, reason):
+ super().addSkip(test, reason)
+ self._print_status(' SKIPPED ', test, reason)
+
+ def addExpectedFailure(self, test, err):
+ super().addExpectedFailure(test, err)
+ self._print_status(' OK ', test)
+
+ def addUnexpectedSuccess(self, test):
+ super().addUnexpectedSuccess(test)
+ self._print_status(' FAILED ', test)
+
+
+class TextTestRunner(unittest.TextTestRunner):
+ """A test runner that produces formatted text results."""
+
+ _TEST_RESULT_CLASS = TextTestResult
+
+ # Set this to true at the class or instance level to run tests using a
+ # debug-friendly method (e.g, one that doesn't catch exceptions and interacts
+ # better with debuggers).
+ # Usually this is set using --pdb_post_mortem.
+ run_for_debugging = False
+
+ def run(self, test) -> unittest.TextTestResult:
+ if self.run_for_debugging:
+ return self._run_debug(test)
+ else:
+ return super().run(test)
+
+ def _run_debug(self, test) -> unittest.TextTestResult:
+ test.debug()
+ # Return an empty result to indicate success.
+ return self._makeResult()
+
+ def _makeResult(self):
+ return TextTestResult(self.stream, self.descriptions, self.verbosity)
diff --git a/venv/Lib/site-packages/absl/testing/absltest.py b/venv/Lib/site-packages/absl/testing/absltest.py
new file mode 100644
index 0000000000000000000000000000000000000000..a56da04233cb7d78cdb6f731ede9ca37311ff27d
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/absltest.py
@@ -0,0 +1,2865 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Base functionality for Abseil Python tests.
+
+This module contains base classes and high-level functions for Abseil-style
+tests.
+"""
+
+from collections import abc
+import contextlib
+import dataclasses
+import difflib
+import enum
+import errno
+import faulthandler
+import getpass
+import inspect
+import io
+import itertools
+import json
+import numbers
+import os
+import random
+import re
+import shlex
+import shutil
+import signal
+import stat
+import subprocess
+import sys
+import tempfile
+import textwrap
+import typing
+from typing import Any, AnyStr, BinaryIO, IO, NoReturn, TextIO
+import unittest
+from unittest import mock # pylint: disable=unused-import Allow absltest.mock.
+import unittest.case
+from urllib import parse
+
+from absl import app # pylint: disable=g-import-not-at-top
+from absl import flags
+from absl import logging
+from absl.testing import _pretty_print_reporter
+from absl.testing import xml_reporter
+
+
+# Re-export a bunch of unittest functions we support so that people don't
+# have to import unittest to get them
+# pylint: disable=invalid-name
+skip = unittest.skip
+skipIf = unittest.skipIf
+skipUnless = unittest.skipUnless
+SkipTest = unittest.SkipTest
+expectedFailure = unittest.expectedFailure
+# pylint: enable=invalid-name
+
+# End unittest re-exports
+
+FLAGS = flags.FLAGS
+
+# Private typing symbols.
+_T = typing.TypeVar('_T') # Unbounded TypeVar for general usage
+_TEXT_OR_BINARY_TYPES = (str, bytes)
+
+# Suppress surplus entries in AssertionError stack traces.
+__unittest = True # pylint: disable=invalid-name
+
+
+def expectedFailureIf(condition, reason): # pylint: disable=invalid-name
+ """Expects the test to fail if the run condition is True.
+
+ Example usage::
+
+ @expectedFailureIf(sys.version.major == 2, "Not yet working in py2")
+ def test_foo(self):
+ ...
+
+ Args:
+ condition: bool, whether to expect failure or not.
+ reason: str, the reason to expect failure.
+
+ Returns:
+ Decorator function
+ """
+ del reason # Unused
+ if condition:
+ return unittest.expectedFailure
+ else:
+ return lambda f: f
+
+
+class TempFileCleanup(enum.Enum):
+ # Always cleanup temp files when the test completes.
+ ALWAYS = 'always'
+ # Only cleanup temp file if the test passes. This allows easier inspection
+ # of tempfile contents on test failure. absltest.TEST_TMPDIR.value determines
+ # where tempfiles are created.
+ SUCCESS = 'success'
+ # Never cleanup temp files.
+ OFF = 'never'
+
+
+# Many of the methods in this module have names like assertSameElements.
+# This kind of name does not comply with PEP8 style,
+# but it is consistent with the naming of methods in unittest.py.
+# pylint: disable=invalid-name
+
+
+def _get_default_test_random_seed() -> int:
+ random_seed = 301
+ value = os.environ.get('TEST_RANDOM_SEED', '')
+ try:
+ random_seed = int(value)
+ except ValueError:
+ pass
+ return random_seed
+
+
+def get_default_test_srcdir() -> str:
+ """Returns default test source dir."""
+ return os.environ.get('TEST_SRCDIR', '')
+
+
+def get_default_test_tmpdir() -> str:
+ """Returns default test temp dir."""
+ tmpdir = os.environ.get('TEST_TMPDIR', '')
+ if not tmpdir:
+ tmpdir = os.path.join(tempfile.gettempdir(), 'absl_testing')
+
+ return tmpdir
+
+
+def _get_default_randomize_ordering_seed() -> int:
+ """Returns default seed to use for randomizing test order.
+
+ This function first checks the --test_randomize_ordering_seed flag, and then
+ the TEST_RANDOMIZE_ORDERING_SEED environment variable. If the first value
+ we find is:
+ * (not set): disable test randomization
+ * 0: disable test randomization
+ * 'random': choose a random seed in [1, 4294967295] for test order
+ randomization
+ * positive integer: use this seed for test order randomization
+
+ (The values used are patterned after
+ https://docs.python.org/3/using/cmdline.html#envvar-PYTHONHASHSEED).
+
+ In principle, it would be simpler to return None if no override is provided;
+ however, the python random module has no `get_seed()`, only `getstate()`,
+ which returns far more data than we want to pass via an environment variable
+ or flag.
+
+ Returns:
+ A default value for test case randomization (int). 0 means do not randomize.
+
+ Raises:
+ ValueError: Raised when the flag or env value is not one of the options
+ above.
+ """
+ if FLAGS['test_randomize_ordering_seed'].present:
+ randomize = FLAGS.test_randomize_ordering_seed
+ elif 'TEST_RANDOMIZE_ORDERING_SEED' in os.environ:
+ randomize = os.environ['TEST_RANDOMIZE_ORDERING_SEED']
+ else:
+ randomize = ''
+ if not randomize:
+ return 0
+ if randomize == 'random':
+ return random.Random().randint(1, 4294967295)
+ if randomize == '0':
+ return 0
+ try:
+ seed = int(randomize)
+ if seed > 0:
+ return seed
+ except ValueError:
+ pass
+ raise ValueError(f'Unknown test randomization seed value: {randomize}')
+
+
+TEST_SRCDIR = flags.DEFINE_string(
+ 'test_srcdir',
+ get_default_test_srcdir(),
+ 'Root of directory tree where source files live',
+ allow_override_cpp=True)
+TEST_TMPDIR = flags.DEFINE_string(
+ 'test_tmpdir',
+ get_default_test_tmpdir(),
+ 'Directory for temporary testing files',
+ allow_override_cpp=True)
+
+flags.DEFINE_integer(
+ 'test_random_seed',
+ _get_default_test_random_seed(),
+ 'Random seed for testing. Some test frameworks may '
+ 'change the default value of this flag between runs, so '
+ 'it is not appropriate for seeding probabilistic tests.',
+ allow_override_cpp=True)
+flags.DEFINE_string(
+ 'test_randomize_ordering_seed',
+ '',
+ 'If positive, use this as a seed to randomize the '
+ 'execution order for test cases. If "random", pick a '
+ 'random seed to use. If 0 or not set, do not randomize '
+ 'test case execution order. This flag also overrides '
+ 'the TEST_RANDOMIZE_ORDERING_SEED environment variable.',
+ allow_override_cpp=True)
+flags.DEFINE_string('xml_output_file', '', 'File to store XML test results')
+
+
+def _open(
+ filepath: str, mode: str, _open_func: abc.Callable[..., IO[AnyStr]] = open
+) -> IO[AnyStr]:
+ """Opens a file.
+
+ Like open(), but ensure that we can open real files even if tests stub out
+ open().
+
+ Args:
+ filepath: A filepath.
+ mode: A mode.
+ _open_func: A built-in open() function.
+
+ Returns:
+ The opened file object.
+ """
+ return _open_func(filepath, mode, encoding='utf-8')
+
+
+class _TempDir:
+ """Represents a temporary directory for tests.
+
+ Creation of this class is internal. Using its public methods is OK.
+
+ This class implements the `os.PathLike[str]` interface. This means it can
+ be directly passed to e.g. `os.path.join()`.
+ """
+
+ def __init__(self, path: str) -> None:
+ """Module-private: do not instantiate outside module."""
+ self._path = path
+
+ @property
+ def full_path(self) -> str:
+ """Returns the path, as a string, for the directory.
+
+ TIP: Instead of e.g. `os.path.join(temp_dir.full_path, some_file_name)`,
+ you can simply do `os.path.join(temp_dir, some_file_name)` because
+ `__fspath__()` is implemented.
+ """
+ return self._path
+
+ def __fspath__(self) -> str:
+ """See os.PathLike."""
+ return self.full_path
+
+ def create_file(
+ self,
+ file_path: str | None = None,
+ content: AnyStr | None = None,
+ mode: str = 'w',
+ encoding: str = 'utf8',
+ errors: str = 'strict',
+ ) -> '_TempFile':
+ """Create a file in the directory.
+
+ NOTE: If the file already exists, it will be made writable and overwritten.
+
+ Args:
+ file_path: Optional file path for the temp file. If not given, a unique
+ file name will be generated and used. Slashes are allowed in the name;
+ any missing intermediate directories will be created. NOTE: This path
+ is the path that will be cleaned up, including any directories in the
+ path, e.g., 'foo/bar/baz.txt' will `rm -r foo`
+ content: Optional string or bytes to initially write to the file. If not
+ specified, then an empty file is created.
+ mode: Mode string to use when writing content. Only used if `content` is
+ non-empty.
+ encoding: Encoding to use when writing string content. Only used if
+ `content` is text.
+ errors: How to handle text to bytes encoding errors. Only used if
+ `content` is text.
+
+ Returns:
+ A _TempFile representing the created file.
+ """
+ tf, _ = _TempFile._create(self._path, file_path, content, mode, encoding,
+ errors)
+ return tf
+
+ def mkdir(self, dir_path: str | None = None) -> '_TempDir':
+ """Create a directory in the directory.
+
+ Args:
+ dir_path: Optional path to the directory to create. If not given,
+ a unique name will be generated and used.
+
+ Returns:
+ A _TempDir representing the created directory.
+ """
+ if dir_path:
+ path = os.path.join(self._path, dir_path)
+ else:
+ path = tempfile.mkdtemp(dir=self._path)
+
+ # Note: there's no need to clear the directory since the containing
+ # dir was cleared by the tempdir() function.
+ os.makedirs(path, exist_ok=True)
+ return _TempDir(path)
+
+
+class _TempFile:
+ """Represents a tempfile for tests.
+
+ Creation of this class is internal. Using its public methods is OK.
+
+ This class implements the `os.PathLike[str]` interface. This means it can
+ be directly passed to e.g. `os.path.join()`.
+ """
+
+ def __init__(self, path: str) -> None:
+ """Private: use _create instead."""
+ self._path = path
+
+ @classmethod
+ def _create(
+ cls,
+ base_path: str,
+ file_path: str | None,
+ content: AnyStr | None,
+ mode: str,
+ encoding: str,
+ errors: str,
+ ) -> tuple['_TempFile', str]:
+ """Module-private: create a tempfile instance."""
+ if file_path:
+ cleanup_path = os.path.join(base_path, _get_first_part(file_path))
+ path = os.path.join(base_path, file_path)
+ os.makedirs(os.path.dirname(path), exist_ok=True)
+ # The file may already exist, in which case, ensure it's writable so that
+ # it can be truncated.
+ if os.path.exists(path) and not os.access(path, os.W_OK):
+ stat_info = os.stat(path)
+ os.chmod(path, stat_info.st_mode | stat.S_IWUSR)
+ else:
+ os.makedirs(base_path, exist_ok=True)
+ fd, path = tempfile.mkstemp(dir=str(base_path))
+ os.close(fd)
+ cleanup_path = path
+
+ tf = cls(path)
+
+ if content:
+ if isinstance(content, str):
+ tf.write_text(content, mode=mode, encoding=encoding, errors=errors)
+ else:
+ tf.write_bytes(content, mode)
+
+ else:
+ tf.write_bytes(b'')
+
+ return tf, cleanup_path
+
+ @property
+ def full_path(self) -> str:
+ """Returns the path, as a string, for the file.
+
+ TIP: Instead of e.g. `os.path.join(temp_file.full_path, some_file_name)`,
+ you can simply do `os.path.join(temp_file, some_file_name)` because
+ `__fspath__()` is implemented.
+ """
+ return self._path
+
+ def __fspath__(self) -> str:
+ """See os.PathLike."""
+ return self.full_path
+
+ def read_text(self, encoding: str = 'utf8', errors: str = 'strict') -> str:
+ """Return the contents of the file as text."""
+ with self.open_text(encoding=encoding, errors=errors) as fp:
+ return fp.read()
+
+ def read_bytes(self) -> bytes:
+ """Return the content of the file as bytes."""
+ with self.open_bytes() as fp:
+ return fp.read()
+
+ def write_text(
+ self,
+ text: str,
+ mode: str = 'w',
+ encoding: str = 'utf8',
+ errors: str = 'strict',
+ ) -> None:
+ """Write text to the file.
+
+ Args:
+ text: Text to write.
+ mode: The mode to open the file for writing.
+ encoding: The encoding to use when writing the text to the file.
+ errors: The error handling strategy to use when converting text to bytes.
+ """
+ with self.open_text(mode, encoding=encoding, errors=errors) as fp:
+ fp.write(text)
+
+ def write_bytes(self, data: bytes, mode: str = 'wb') -> None:
+ """Write bytes to the file.
+
+ Args:
+ data: bytes to write.
+ mode: Mode to open the file for writing. The "b" flag is implicit if
+ not already present. It must not have the "t" flag.
+ """
+ with self.open_bytes(mode) as fp:
+ fp.write(data)
+
+ def open_text(
+ self, mode: str = 'rt', encoding: str = 'utf8', errors: str = 'strict'
+ ) -> contextlib.AbstractContextManager[TextIO]:
+ """Return a context manager for opening the file in text mode.
+
+ Args:
+ mode: The mode to open the file in. The "t" flag is implicit if not
+ already present. It must not have the "b" flag.
+ encoding: The encoding to use when opening the file.
+ errors: How to handle decoding errors.
+
+ Returns:
+ Context manager that yields an open file.
+
+ Raises:
+ ValueError: if invalid inputs are provided.
+ """
+ if 'b' in mode:
+ raise ValueError('Invalid mode {!r}: "b" flag not allowed when opening '
+ 'file in text mode'.format(mode))
+ if 't' not in mode:
+ mode += 't'
+ cm = self._open(mode, encoding, errors)
+ return cm
+
+ def open_bytes(
+ self, mode: str = 'rb'
+ ) -> contextlib.AbstractContextManager[BinaryIO]:
+ """Return a context manager for opening the file in binary mode.
+
+ Args:
+ mode: The mode to open the file in. The "b" mode is implicit if not
+ already present. It must not have the "t" flag.
+
+ Returns:
+ Context manager that yields an open file.
+
+ Raises:
+ ValueError: if invalid inputs are provided.
+ """
+ if 't' in mode:
+ raise ValueError('Invalid mode {!r}: "t" flag not allowed when opening '
+ 'file in binary mode'.format(mode))
+ if 'b' not in mode:
+ mode += 'b'
+ cm = self._open(mode, encoding=None, errors=None)
+ return cm
+
+ # TODO(b/123775699): Once pytype supports typing.Literal, use overload and
+ # Literal to express more precise return types. The contained type is
+ # currently `Any` to avoid [bad-return-type] errors in the open_* methods.
+ @contextlib.contextmanager
+ def _open(
+ self,
+ mode: str,
+ encoding: str | None = 'utf8',
+ errors: str | None = 'strict',
+ ) -> abc.Iterator[Any]:
+ with open(
+ self.full_path, mode=mode, encoding=encoding, errors=errors
+ ) as fp:
+ yield fp
+
+
+class _method:
+ """A decorator that supports both instance and classmethod invocations.
+
+ Using similar semantics to the @property builtin, this decorator can augment
+ an instance method to support conditional logic when invoked on a class
+ object. This breaks support for invoking an instance method via the class
+ (e.g. Cls.method(self, ...)) but is still situationally useful.
+ """
+
+ _finstancemethod: Any
+ _fclassmethod: Any | None
+
+ def __init__(self, finstancemethod: abc.Callable[..., Any]) -> None:
+ self._finstancemethod = finstancemethod
+ self._fclassmethod = None
+
+ def classmethod(self, fclassmethod: abc.Callable[..., Any]) -> '_method':
+ if isinstance(fclassmethod, classmethod):
+ self._fclassmethod = fclassmethod
+ else:
+ self._fclassmethod = classmethod(fclassmethod)
+ return self
+
+ def __doc__(self) -> str: # type: ignore[override]
+ return (
+ getattr(self._finstancemethod, '__doc__')
+ or getattr(self._fclassmethod, '__doc__')
+ or ''
+ )
+
+ def __get__(
+ self, obj: Any | None, type_: type[Any] | None
+ ) -> abc.Callable[..., Any]:
+ func = self._fclassmethod if obj is None else self._finstancemethod
+ return func.__get__(obj, type_) # type: ignore[attribute-error, union-attr]
+
+
+class TestCase(unittest.TestCase):
+ """Extension of unittest.TestCase providing more power."""
+
+ # When to cleanup files/directories created by our `create_tempfile()` and
+ # `create_tempdir()` methods after each test case completes. This does *not*
+ # affect e.g., files created outside of those methods, e.g., using the stdlib
+ # tempfile module. This can be overridden at the class level, instance level,
+ # or with the `cleanup` arg of `create_tempfile()` and `create_tempdir()`. See
+ # `TempFileCleanup` for details on the different values.
+ tempfile_cleanup: TempFileCleanup = TempFileCleanup.ALWAYS
+
+ maxDiff = 80 * 20
+ longMessage = True
+
+ # Exit stacks for per-test and per-class scopes.
+ if sys.version_info < (3, 11):
+ _exit_stack = None
+ _cls_exit_stack = None
+
+ def __init__(self, *args, **kwargs) -> None:
+ super().__init__(*args, **kwargs)
+ # This is to work around missing type stubs in unittest.pyi
+ self._outcome: Any | None = getattr(self, '_outcome')
+
+ def setUp(self):
+ super().setUp()
+ # NOTE: Only Python 3 contextlib has ExitStack and
+ # Python 3.11+ already has enterContext.
+ if hasattr(contextlib, 'ExitStack') and sys.version_info < (3, 11):
+ self._exit_stack = contextlib.ExitStack()
+ self.addCleanup(self._exit_stack.close)
+
+ @classmethod
+ def setUpClass(cls):
+ super().setUpClass()
+ # NOTE: Only Python 3 contextlib has ExitStack, only Python 3.8+ has
+ # addClassCleanup and Python 3.11+ already has enterClassContext.
+ if (
+ hasattr(contextlib, 'ExitStack')
+ and hasattr(cls, 'addClassCleanup')
+ and sys.version_info < (3, 11)
+ ):
+ cls._cls_exit_stack = contextlib.ExitStack()
+ cls.addClassCleanup(cls._cls_exit_stack.close)
+
+ def create_tempdir(
+ self,
+ name: str | None = None,
+ cleanup: TempFileCleanup | None = None,
+ ) -> _TempDir:
+ """Create a temporary directory specific to the test.
+
+ NOTE: The directory and its contents will be recursively cleared before
+ creation. This ensures that there is no pre-existing state.
+
+ This creates a named directory on disk that is isolated to this test, and
+ will be properly cleaned up by the test. This avoids several pitfalls of
+ creating temporary directories for test purposes, as well as makes it easier
+ to setup directories and verify their contents. For example::
+
+ def test_foo(self):
+ out_dir = self.create_tempdir()
+ out_log = out_dir.create_file('output.log')
+ expected_outputs = [
+ os.path.join(out_dir, 'data-0.txt'),
+ os.path.join(out_dir, 'data-1.txt'),
+ ]
+ code_under_test(out_dir)
+ self.assertTrue(os.path.exists(expected_paths[0]))
+ self.assertTrue(os.path.exists(expected_paths[1]))
+ self.assertEqual('foo', out_log.read_text())
+
+ See also: :meth:`create_tempfile` for creating temporary files.
+
+ Args:
+ name: Optional name of the directory. If not given, a unique
+ name will be generated and used.
+ cleanup: Optional cleanup policy on when/if to remove the directory (and
+ all its contents) at the end of the test. If None, then uses
+ :attr:`tempfile_cleanup`.
+
+ Returns:
+ A _TempDir representing the created directory; see _TempDir class docs
+ for usage.
+ """
+ test_path = self._get_tempdir_path_test()
+
+ if name:
+ path = os.path.join(test_path, name)
+ cleanup_path = os.path.join(test_path, _get_first_part(name))
+ else:
+ os.makedirs(test_path, exist_ok=True)
+ path = tempfile.mkdtemp(dir=test_path)
+ cleanup_path = path
+
+ _rmtree_ignore_errors(cleanup_path)
+ os.makedirs(path, exist_ok=True)
+
+ self._maybe_add_temp_path_cleanup(cleanup_path, cleanup)
+
+ return _TempDir(path)
+
+ def create_tempfile(
+ self,
+ file_path: str | None = None,
+ content: AnyStr | None = None,
+ mode: str = 'w',
+ encoding: str = 'utf8',
+ errors: str = 'strict',
+ cleanup: TempFileCleanup | None = None,
+ ) -> _TempFile:
+ """Create a temporary file specific to the test.
+
+ This creates a named file on disk that is isolated to this test, and will
+ be properly cleaned up by the test. This avoids several pitfalls of
+ creating temporary files for test purposes, as well as makes it easier
+ to setup files, their data, read them back, and inspect them when
+ a test fails. For example::
+
+ def test_foo(self):
+ output = self.create_tempfile()
+ code_under_test(output)
+ self.assertGreater(os.path.getsize(output), 0)
+ self.assertEqual('foo', output.read_text())
+
+ NOTE: This will zero-out the file. This ensures there is no pre-existing
+ state.
+ NOTE: If the file already exists, it will be made writable and overwritten.
+
+ See also: :meth:`create_tempdir` for creating temporary directories, and
+ ``_TempDir.create_file`` for creating files within a temporary directory.
+
+ Args:
+ file_path: Optional file path for the temp file. If not given, a unique
+ file name will be generated and used. Slashes are allowed in the name;
+ any missing intermediate directories will be created. NOTE: This path is
+ the path that will be cleaned up, including any directories in the path,
+ e.g., ``'foo/bar/baz.txt'`` will ``rm -r foo``.
+ content: Optional string or
+ bytes to initially write to the file. If not
+ specified, then an empty file is created.
+ mode: Mode string to use when writing content. Only used if `content` is
+ non-empty.
+ encoding: Encoding to use when writing string content. Only used if
+ `content` is text.
+ errors: How to handle text to bytes encoding errors. Only used if
+ `content` is text.
+ cleanup: Optional cleanup policy on when/if to remove the directory (and
+ all its contents) at the end of the test. If None, then uses
+ :attr:`tempfile_cleanup`.
+
+ Returns:
+ A _TempFile representing the created file; see _TempFile class docs for
+ usage.
+ """
+ test_path = self._get_tempdir_path_test()
+ tf, cleanup_path = _TempFile._create(test_path, file_path, content=content,
+ mode=mode, encoding=encoding,
+ errors=errors)
+ self._maybe_add_temp_path_cleanup(cleanup_path, cleanup)
+ return tf
+
+ @_method
+ def enter_context(self, manager: contextlib.AbstractContextManager[_T]) -> _T:
+ """Returns the CM's value after registering it with the exit stack.
+
+ Entering a context pushes it onto a stack of contexts. When `enter_context`
+ is called on the test instance (e.g. `self.enter_context`), the context is
+ exited after the test case's tearDown call. When called on the test class
+ (e.g. `TestCase.enter_context`), the context is exited after the test
+ class's tearDownClass call.
+
+ Contexts are exited in the reverse order of entering. They will always
+ be exited, regardless of test failure/success.
+
+ This is useful to eliminate per-test boilerplate when context managers
+ are used. For example, instead of decorating every test with `@mock.patch`,
+ simply do `self.foo = self.enter_context(mock.patch(...))' in `setUp()`.
+
+ NOTE: The context managers will always be exited without any error
+ information. This is an unfortunate implementation detail due to some
+ internals of how unittest runs tests.
+
+ Args:
+ manager: The context manager to enter.
+ """
+ if sys.version_info >= (3, 11):
+ return self.enterContext(manager)
+
+ if not self._exit_stack:
+ raise AssertionError(
+ 'self._exit_stack is not set: enter_context is Py3-only; also make '
+ 'sure that AbslTest.setUp() is called.')
+ return self._exit_stack.enter_context(manager)
+
+ @enter_context.classmethod
+ @classmethod
+ def _enter_context_cls(
+ cls, manager: contextlib.AbstractContextManager[_T]
+ ) -> _T:
+ if sys.version_info >= (3, 11):
+ return cls.enterClassContext(manager)
+
+ if not cls._cls_exit_stack:
+ raise AssertionError(
+ 'cls._cls_exit_stack is not set: cls.enter_context requires '
+ 'Python 3.8+; also make sure that AbslTest.setUpClass() is called.')
+ return cls._cls_exit_stack.enter_context(manager)
+
+ @classmethod
+ def _get_tempdir_path_cls(cls) -> str:
+ return os.path.join(TEST_TMPDIR.value,
+ cls.__qualname__.replace('__main__.', ''))
+
+ def _get_tempdir_path_test(self) -> str:
+ return os.path.join(self._get_tempdir_path_cls(), self._testMethodName)
+
+ def _get_tempfile_cleanup(
+ self, override: TempFileCleanup | None
+ ) -> TempFileCleanup:
+ if override is not None:
+ return override
+ return self.tempfile_cleanup
+
+ def _maybe_add_temp_path_cleanup(
+ self, path: str, cleanup: TempFileCleanup | None
+ ) -> None:
+ cleanup = self._get_tempfile_cleanup(cleanup)
+ if cleanup == TempFileCleanup.OFF:
+ return
+ elif cleanup == TempFileCleanup.ALWAYS:
+ self.addCleanup(_rmtree_ignore_errors, path)
+ elif cleanup == TempFileCleanup.SUCCESS:
+ self._internal_add_cleanup_on_success(_rmtree_ignore_errors, path)
+ else:
+ raise AssertionError(f'Unexpected cleanup value: {cleanup}')
+
+ def _internal_add_cleanup_on_success(
+ self,
+ function: abc.Callable[..., Any],
+ *args: Any,
+ **kwargs: Any,
+ ) -> None:
+ """Adds `function` as cleanup when the test case succeeds."""
+ outcome = self._outcome
+ assert outcome is not None
+ previous_failure_count = (
+ len(outcome.result.failures)
+ + len(outcome.result.errors)
+ + len(outcome.result.unexpectedSuccesses)
+ )
+ def _call_cleaner_on_success(*args, **kwargs):
+ if not self._internal_ran_and_passed_when_called_during_cleanup(
+ previous_failure_count):
+ return
+ function(*args, **kwargs)
+ self.addCleanup(_call_cleaner_on_success, *args, **kwargs)
+
+ def _internal_ran_and_passed_when_called_during_cleanup(
+ self,
+ previous_failure_count: int,
+ ) -> bool:
+ """Returns whether test is passed. Expected to be called during cleanup."""
+ outcome = self._outcome
+ if sys.version_info[:2] >= (3, 11):
+ assert outcome is not None
+ current_failure_count = (
+ len(outcome.result.failures)
+ + len(outcome.result.errors)
+ + len(outcome.result.unexpectedSuccesses)
+ )
+ return current_failure_count == previous_failure_count
+ else:
+ # Before Python 3.11 https://github.com/python/cpython/pull/28180, errors
+ # were bufferred in _Outcome before calling cleanup.
+ result = self.defaultTestResult()
+ self._feedErrorsToResult(result, outcome.errors) # pytype: disable=attribute-error
+ return result.wasSuccessful()
+
+ def shortDescription(self) -> str:
+ """Formats both the test method name and the first line of its docstring.
+
+ If no docstring is given, only returns the method name.
+
+ This method overrides unittest.TestCase.shortDescription(), which
+ only returns the first line of the docstring, obscuring the name
+ of the test upon failure.
+
+ Returns:
+ desc: A short description of a test method.
+ """
+ desc = self.id()
+
+ # Omit the main name so that test name can be directly copy/pasted to
+ # the command line.
+ if desc.startswith('__main__.'):
+ desc = desc[len('__main__.'):]
+
+ # NOTE: super() is used here instead of directly invoking
+ # unittest.TestCase.shortDescription(self), because of the
+ # following line that occurs later on:
+ # unittest.TestCase = TestCase
+ # Because of this, direct invocation of what we think is the
+ # superclass will actually cause infinite recursion.
+ doc_first_line = super().shortDescription()
+ if doc_first_line is not None:
+ desc = '\n'.join((desc, doc_first_line))
+ return desc
+
+ def assertStartsWith(self, actual, expected_start, msg=None):
+ """Asserts that actual.startswith(expected_start) is True.
+
+ Args:
+ actual: str
+ expected_start: str
+ msg: Optional message to report on failure.
+ """
+ if not actual.startswith(expected_start):
+ self.fail('%r does not start with %r' % (actual, expected_start), msg)
+
+ def assertNotStartsWith(self, actual, unexpected_start, msg=None):
+ """Asserts that actual.startswith(unexpected_start) is False.
+
+ Args:
+ actual: str
+ unexpected_start: str
+ msg: Optional message to report on failure.
+ """
+ if actual.startswith(unexpected_start):
+ self.fail('%r does start with %r' % (actual, unexpected_start), msg)
+
+ def assertEndsWith(self, actual, expected_end, msg=None):
+ """Asserts that actual.endswith(expected_end) is True.
+
+ Args:
+ actual: str
+ expected_end: str
+ msg: Optional message to report on failure.
+ """
+ if not actual.endswith(expected_end):
+ self.fail('%r does not end with %r' % (actual, expected_end), msg)
+
+ def assertNotEndsWith(self, actual, unexpected_end, msg=None):
+ """Asserts that actual.endswith(unexpected_end) is False.
+
+ Args:
+ actual: str
+ unexpected_end: str
+ msg: Optional message to report on failure.
+ """
+ if actual.endswith(unexpected_end):
+ self.fail('%r does end with %r' % (actual, unexpected_end), msg)
+
+ def assertSequenceStartsWith(self, prefix, whole, msg=None):
+ """An equality assertion for the beginning of ordered sequences.
+
+ If prefix is an empty sequence, it will raise an error unless whole is also
+ an empty sequence.
+
+ If prefix is not a sequence, it will raise an error if the first element of
+ whole does not match.
+
+ Args:
+ prefix: A sequence expected at the beginning of the whole parameter.
+ whole: The sequence in which to look for prefix.
+ msg: Optional message to report on failure.
+ """
+ try:
+ prefix_len = len(prefix)
+ except (TypeError, NotImplementedError):
+ prefix = [prefix]
+ prefix_len = 1
+
+ if isinstance(whole, abc.Mapping) or isinstance(whole, abc.Set):
+ self.fail(
+ 'For whole: Mapping or Set objects are not supported, found type: %s'
+ % type(whole),
+ msg,
+ )
+ try:
+ whole_len = len(whole)
+ except (TypeError, NotImplementedError):
+ self.fail('For whole: len(%s) is not supported, it appears to be type: '
+ '%s' % (whole, type(whole)), msg)
+
+ assert prefix_len <= whole_len, self._formatMessage(
+ msg,
+ 'Prefix length (%d) is longer than whole length (%d).' %
+ (prefix_len, whole_len)
+ )
+
+ if not prefix_len and whole_len:
+ self.fail('Prefix length is 0 but whole length is %d: %s' %
+ (len(whole), whole), msg)
+
+ try:
+ self.assertSequenceEqual(prefix, whole[:prefix_len], msg)
+ except AssertionError:
+ self.fail('prefix: %s not found at start of whole: %s.' %
+ (prefix, whole), msg)
+
+ def assertEmpty(self, container, msg=None):
+ """Asserts that an object has zero length.
+
+ Args:
+ container: Anything that implements the collections.abc.Sized interface.
+ msg: Optional message to report on failure.
+ """
+ if not isinstance(container, abc.Sized):
+ self.fail('Expected a Sized object, got: '
+ '{!r}'.format(type(container).__name__), msg)
+
+ # explicitly check the length since some Sized objects (e.g. numpy.ndarray)
+ # have strange __nonzero__/__bool__ behavior.
+ if len(container): # pylint: disable=g-explicit-length-test
+ self.fail(f'{container!r} has length of {len(container)}.', msg)
+
+ def assertNotEmpty(self, container, msg=None):
+ """Asserts that an object has non-zero length.
+
+ Args:
+ container: Anything that implements the collections.abc.Sized interface.
+ msg: Optional message to report on failure.
+ """
+ if not isinstance(container, abc.Sized):
+ self.fail('Expected a Sized object, got: '
+ '{!r}'.format(type(container).__name__), msg)
+
+ # explicitly check the length since some Sized objects (e.g. numpy.ndarray)
+ # have strange __nonzero__/__bool__ behavior.
+ if not len(container): # pylint: disable=g-explicit-length-test
+ self.fail(f'{container!r} has length of 0.', msg)
+
+ def assertLen(self, container, expected_len, msg=None):
+ """Asserts that an object has the expected length.
+
+ Args:
+ container: Anything that implements the collections.abc.Sized interface.
+ expected_len: The expected length of the container.
+ msg: Optional message to report on failure.
+ """
+ if not isinstance(container, abc.Sized):
+ self.fail('Expected a Sized object, got: '
+ '{!r}'.format(type(container).__name__), msg)
+ if len(container) != expected_len:
+ container_repr = unittest.util.safe_repr(container) # pytype: disable=module-attr
+ self.fail('{} has length of {}, expected {}.'.format(
+ container_repr, len(container), expected_len), msg)
+
+ def assertSequenceAlmostEqual(self, expected_seq, actual_seq, places=None,
+ msg=None, delta=None):
+ """An approximate equality assertion for ordered sequences.
+
+ Fail if the two sequences are unequal as determined by their value
+ differences rounded to the given number of decimal places (default 7) and
+ comparing to zero, or by comparing that the difference between each value
+ in the two sequences is more than the given delta.
+
+ Note that decimal places (from zero) are usually not the same as significant
+ digits (measured from the most significant digit).
+
+ If the two sequences compare equal then they will automatically compare
+ almost equal.
+
+ Args:
+ expected_seq: A sequence containing elements we are expecting.
+ actual_seq: The sequence that we are testing.
+ places: The number of decimal places to compare.
+ msg: The message to be printed if the test fails.
+ delta: The OK difference between compared values.
+ """
+ if len(expected_seq) != len(actual_seq):
+ self.fail('Sequence size mismatch: {} vs {}'.format(
+ len(expected_seq), len(actual_seq)), msg)
+
+ err_list = []
+ for idx, (exp_elem, act_elem) in enumerate(zip(expected_seq, actual_seq)):
+ try:
+ # assertAlmostEqual should be called with at most one of `places` and
+ # `delta`. However, it's okay for assertSequenceAlmostEqual to pass
+ # both because we want the latter to fail if the former does.
+ # pytype: disable=wrong-keyword-args
+ self.assertAlmostEqual(exp_elem, act_elem, places=places, msg=msg,
+ delta=delta)
+ # pytype: enable=wrong-keyword-args
+ except self.failureException as err:
+ err_list.append(f'At index {idx}: {err}')
+
+ if err_list:
+ if len(err_list) > 30:
+ err_list = err_list[:30] + ['...']
+ msg = self._formatMessage(msg, '\n'.join(err_list))
+ self.fail(msg)
+
+ def assertContainsSubset(self, expected_subset, actual_set, msg=None):
+ """Checks whether actual iterable is a superset of expected iterable."""
+ missing = set(expected_subset) - set(actual_set)
+ if not missing:
+ return
+
+ self.fail('Missing elements %s\nExpected: %s\nActual: %s' % (
+ missing, expected_subset, actual_set), msg)
+
+ def assertNoCommonElements(self, expected_seq, actual_seq, msg=None):
+ """Checks whether actual iterable and expected iterable are disjoint."""
+ common = set(expected_seq) & set(actual_seq)
+ if not common:
+ return
+
+ self.fail('Common elements %s\nExpected: %s\nActual: %s' % (
+ common, expected_seq, actual_seq), msg)
+
+ def assertItemsEqual(self, expected_seq, actual_seq, msg=None):
+ """Deprecated, please use assertCountEqual instead.
+
+ This is equivalent to assertCountEqual.
+
+ Args:
+ expected_seq: A sequence containing elements we are expecting.
+ actual_seq: The sequence that we are testing.
+ msg: The message to be printed if the test fails.
+ """
+ super().assertCountEqual(expected_seq, actual_seq, msg)
+
+ def assertSameElements(self, expected_seq, actual_seq, msg=None):
+ """Asserts that two sequences have the same elements (in any order).
+
+ This method, unlike assertCountEqual, doesn't care about any
+ duplicates in the expected and actual sequences::
+
+ # Doesn't raise an AssertionError
+ assertSameElements([1, 1, 1, 0, 0, 0], [0, 1])
+
+ If possible, you should use assertCountEqual instead of
+ assertSameElements.
+
+ Args:
+ expected_seq: A sequence containing elements we are expecting.
+ actual_seq: The sequence that we are testing.
+ msg: The message to be printed if the test fails.
+ """
+ # `unittest2.TestCase` used to have assertSameElements, but it was
+ # removed in favor of assertItemsEqual. As there's a unit test
+ # that explicitly checks this behavior, I am leaving this method
+ # alone.
+ # Fail on strings: empirically, passing strings to this test method
+ # is almost always a bug. If comparing the character sets of two strings
+ # is desired, cast the inputs to sets or lists explicitly.
+ if (isinstance(expected_seq, _TEXT_OR_BINARY_TYPES) or
+ isinstance(actual_seq, _TEXT_OR_BINARY_TYPES)):
+ self.fail('Passing string/bytes to assertSameElements is usually a bug. '
+ 'Did you mean to use assertEqual?\n'
+ 'Expected: %s\nActual: %s' % (expected_seq, actual_seq))
+ try:
+ expected = {element: None for element in expected_seq}
+ actual = {element: None for element in actual_seq}
+ missing = [element for element in expected if element not in actual]
+ unexpected = [element for element in actual if element not in expected]
+ missing.sort()
+ unexpected.sort()
+ except TypeError:
+ # Fall back to slower list-compare if any of the objects are
+ # not hashable.
+ expected = list(expected_seq)
+ actual = list(actual_seq)
+ expected.sort()
+ actual.sort()
+ missing, unexpected = _sorted_list_difference(expected, actual)
+ errors = []
+ if msg:
+ errors.extend((msg, ':\n'))
+ if missing:
+ errors.append('Expected, but missing:\n %r\n' % missing)
+ if unexpected:
+ errors.append('Unexpected, but present:\n %r\n' % unexpected)
+ if missing or unexpected:
+ self.fail(''.join(errors))
+
+ # unittest.TestCase.assertMultiLineEqual works very similarly, but it
+ # has a different error format. However, I find this slightly more readable.
+ def assertMultiLineEqual(self, first, second, msg=None, **kwargs):
+ """Asserts that two multi-line strings are equal."""
+ assert isinstance(first,
+ str), ('First argument is not a string: %r' % (first,))
+ assert isinstance(second,
+ str), ('Second argument is not a string: %r' % (second,))
+ line_limit = kwargs.pop('line_limit', 0)
+ if kwargs:
+ raise TypeError(f'Unexpected keyword args {tuple(kwargs)}')
+
+ if first == second:
+ return
+ if msg:
+ failure_message = [msg + ':\n']
+ else:
+ failure_message = ['\n']
+ if line_limit:
+ line_limit += len(failure_message)
+ for line in difflib.ndiff(first.splitlines(True), second.splitlines(True)):
+ failure_message.append(line)
+ if not line.endswith('\n'):
+ failure_message.append('\n')
+ if line_limit and len(failure_message) > line_limit:
+ n_omitted = len(failure_message) - line_limit
+ failure_message = failure_message[:line_limit]
+ failure_message.append(
+ '(... and {} more delta lines omitted for brevity.)\n'.format(
+ n_omitted))
+
+ raise self.failureException(''.join(failure_message))
+
+ def assertBetween(self, value, minv, maxv, msg=None):
+ """Asserts that value is between minv and maxv (inclusive)."""
+ msg = self._formatMessage(msg,
+ '"%r" unexpectedly not between "%r" and "%r"' %
+ (value, minv, maxv))
+ self.assertTrue(minv <= value, msg)
+ self.assertTrue(maxv >= value, msg)
+
+ def assertRegexMatch(self, actual_str, regexes, message=None):
+ r"""Asserts that at least one regex in regexes matches str.
+
+ If possible you should use `assertRegex`, which is a simpler
+ version of this method. `assertRegex` takes a single regular
+ expression (a string or re compiled object) instead of a list.
+
+ Notes:
+
+ 1. This function uses substring matching, i.e. the matching
+ succeeds if *any* substring of the error message matches *any*
+ regex in the list. This is more convenient for the user than
+ full-string matching.
+
+ 2. If regexes is the empty list, the matching will always fail.
+
+ 3. Use regexes=[''] for a regex that will always pass.
+
+ 4. '.' matches any single character *except* the newline. To
+ match any character, use '(.|\n)'.
+
+ 5. '^' matches the beginning of each line, not just the beginning
+ of the string. Similarly, '$' matches the end of each line.
+
+ 6. An exception will be thrown if regexes contains an invalid
+ regex.
+
+ Args:
+ actual_str: The string we try to match with the items in regexes.
+ regexes: The regular expressions we want to match against str.
+ See "Notes" above for detailed notes on how this is interpreted.
+ message: The message to be printed if the test fails.
+ """
+ if isinstance(regexes, _TEXT_OR_BINARY_TYPES):
+ self.fail('regexes is string or bytes; use assertRegex instead.',
+ message)
+ if not regexes:
+ self.fail('No regexes specified.', message)
+
+ regex_type = type(regexes[0])
+ for regex in regexes[1:]:
+ if type(regex) is not regex_type: # pylint: disable=unidiomatic-typecheck
+ self.fail('regexes list must all be the same type.', message)
+
+ if regex_type is bytes and isinstance(actual_str, str):
+ regexes = [regex.decode('utf-8') for regex in regexes]
+ regex_type = str
+ elif regex_type is str and isinstance(actual_str, bytes):
+ regexes = [regex.encode('utf-8') for regex in regexes]
+ regex_type = bytes
+
+ if regex_type is str:
+ regex = '(?:%s)' % ')|(?:'.join(regexes)
+ elif regex_type is bytes:
+ regex = b'(?:' + (b')|(?:'.join(regexes)) + b')'
+ else:
+ self.fail('Only know how to deal with unicode str or bytes regexes.',
+ message)
+
+ if not re.search(regex, actual_str, re.MULTILINE):
+ self.fail('"%s" does not contain any of these regexes: %s.' %
+ (actual_str, regexes), message)
+
+ def assertCommandSucceeds(self, command, regexes=(b'',), env=None,
+ close_fds=True, msg=None):
+ """Asserts that a shell command succeeds (i.e. exits with code 0).
+
+ Args:
+ command: List or string representing the command to run.
+ regexes: List of regular expression byte strings that match success.
+ env: Dictionary of environment variable settings. If None, no environment
+ variables will be set for the child process. This is to make tests
+ more hermetic. NOTE: this behavior is different than the standard
+ subprocess module.
+ close_fds: Whether or not to close all open fd's in the child after
+ forking.
+ msg: Optional message to report on failure.
+ """
+ (ret_code, err) = get_command_stderr(command, env, close_fds)
+
+ # We need bytes regexes here because `err` is bytes.
+ # Accommodate code which listed their output regexes w/o the b'' prefix by
+ # converting them to bytes for the user.
+ if isinstance(regexes[0], str):
+ regexes = [regex.encode('utf-8') for regex in regexes]
+
+ command_string = get_command_string(command)
+ self.assertEqual(
+ ret_code, 0,
+ self._formatMessage(msg,
+ 'Running command\n'
+ '%s failed with error code %s and message\n'
+ '%s' % (_quote_long_string(command_string),
+ ret_code,
+ _quote_long_string(err)))
+ )
+ self.assertRegexMatch(
+ err,
+ regexes,
+ message=self._formatMessage(
+ msg,
+ 'Running command\n'
+ '%s failed with error code %s and message\n'
+ '%s which matches no regex in %s' % (
+ _quote_long_string(command_string),
+ ret_code,
+ _quote_long_string(err),
+ regexes)))
+
+ def assertCommandFails(self, command, regexes, env=None, close_fds=True,
+ msg=None):
+ """Asserts a shell command fails and the error matches a regex in a list.
+
+ Args:
+ command: List or string representing the command to run.
+ regexes: the list of regular expression strings.
+ env: Dictionary of environment variable settings. If None, no environment
+ variables will be set for the child process. This is to make tests
+ more hermetic. NOTE: this behavior is different than the standard
+ subprocess module.
+ close_fds: Whether or not to close all open fd's in the child after
+ forking.
+ msg: Optional message to report on failure.
+ """
+ (ret_code, err) = get_command_stderr(command, env, close_fds)
+
+ # We need bytes regexes here because `err` is bytes.
+ # Accommodate code which listed their output regexes w/o the b'' prefix by
+ # converting them to bytes for the user.
+ if isinstance(regexes[0], str):
+ regexes = [regex.encode('utf-8') for regex in regexes]
+
+ command_string = get_command_string(command)
+ self.assertNotEqual(
+ ret_code, 0,
+ self._formatMessage(msg, 'The following command succeeded '
+ 'while expected to fail:\n%s' %
+ _quote_long_string(command_string)))
+ self.assertRegexMatch(
+ err,
+ regexes,
+ message=self._formatMessage(
+ msg,
+ 'Running command\n'
+ '%s failed with error code %s and message\n'
+ '%s which matches no regex in %s' % (
+ _quote_long_string(command_string),
+ ret_code,
+ _quote_long_string(err),
+ regexes)))
+
+ class _AssertRaisesContext:
+
+ def __init__(self, expected_exception, test_case, test_func, msg=None):
+ self.expected_exception = expected_exception
+ self.test_case = test_case
+ self.test_func = test_func
+ self.msg = msg
+
+ def __enter__(self):
+ return self
+
+ def __exit__(self, exc_type, exc_value, tb):
+ if exc_type is None:
+ self.test_case.fail(self.expected_exception.__name__ + ' not raised',
+ self.msg)
+ if not issubclass(exc_type, self.expected_exception):
+ return False
+ self.test_func(exc_value)
+ if exc_value:
+ self.exception = exc_value.with_traceback(None)
+ return True
+
+ @typing.overload
+ def assertRaisesWithPredicateMatch(
+ self, expected_exception, predicate) -> _AssertRaisesContext:
+ # The purpose of this return statement is to work around
+ # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored.
+ return self._AssertRaisesContext(None, None, None)
+
+ @typing.overload
+ def assertRaisesWithPredicateMatch(
+ self,
+ expected_exception,
+ predicate,
+ callable_obj: abc.Callable[..., Any],
+ *args,
+ **kwargs,
+ ) -> None:
+ # The purpose of this return statement is to work around
+ # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored.
+ return self._AssertRaisesContext(None, None, None) # type: ignore[return-value]
+
+ def assertRaisesWithPredicateMatch(self, expected_exception, predicate,
+ callable_obj=None, *args, **kwargs):
+ """Asserts that exception is thrown and predicate(exception) is true.
+
+ Args:
+ expected_exception: Exception class expected to be raised.
+ predicate: Function of one argument that inspects the passed-in exception
+ and returns True (success) or False (please fail the test).
+ callable_obj: Function to be called.
+ *args: Extra args.
+ **kwargs: Extra keyword args.
+
+ Returns:
+ A context manager if callable_obj is None. Otherwise, None.
+
+ Raises:
+ self.failureException if callable_obj does not raise a matching exception.
+ """
+ def Check(err):
+ self.assertTrue(predicate(err),
+ '%r does not match predicate %r' % (err, predicate))
+
+ context = self._AssertRaisesContext(expected_exception, self, Check)
+ if callable_obj is None:
+ return context
+ with context:
+ callable_obj(*args, **kwargs)
+
+ @typing.overload
+ def assertRaisesWithLiteralMatch(
+ self, expected_exception, expected_exception_message
+ ) -> _AssertRaisesContext:
+ # The purpose of this return statement is to work around
+ # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored.
+ return self._AssertRaisesContext(None, None, None)
+
+ @typing.overload
+ def assertRaisesWithLiteralMatch(
+ self,
+ expected_exception,
+ expected_exception_message,
+ callable_obj: abc.Callable[..., Any],
+ *args,
+ **kwargs,
+ ) -> None:
+ # The purpose of this return statement is to work around
+ # https://github.com/PyCQA/pylint/issues/5273; it is otherwise ignored.
+ return self._AssertRaisesContext(None, None, None) # type: ignore[return-value]
+
+ def assertRaisesWithLiteralMatch(self, expected_exception,
+ expected_exception_message,
+ callable_obj=None, *args, **kwargs):
+ """Asserts that the message in a raised exception equals the given string.
+
+ Unlike assertRaisesRegex, this method takes a literal string, not
+ a regular expression.
+
+ with self.assertRaisesWithLiteralMatch(ExType, 'message'):
+ DoSomething()
+
+ Args:
+ expected_exception: Exception class expected to be raised.
+ expected_exception_message: String message expected in the raised
+ exception. For a raise exception e, expected_exception_message must
+ equal str(e).
+ callable_obj: Function to be called, or None to return a context.
+ *args: Extra args.
+ **kwargs: Extra kwargs.
+
+ Returns:
+ A context manager if callable_obj is None. Otherwise, None.
+
+ Raises:
+ self.failureException if callable_obj does not raise a matching exception.
+ """
+ def Check(err):
+ actual_exception_message = str(err)
+ self.assertTrue(expected_exception_message == actual_exception_message,
+ 'Exception message does not match.\n'
+ 'Expected: %r\n'
+ 'Actual: %r' % (expected_exception_message,
+ actual_exception_message))
+
+ context = self._AssertRaisesContext(expected_exception, self, Check)
+ if callable_obj is None:
+ return context
+ with context:
+ callable_obj(*args, **kwargs)
+
+ def assertContainsInOrder(self, strings, target, msg=None):
+ """Asserts that the strings provided are found in the target in order.
+
+ This may be useful for checking HTML output.
+
+ Args:
+ strings: A list of strings, such as [ 'fox', 'dog' ]
+ target: A target string in which to look for the strings, such as
+ 'The quick brown fox jumped over the lazy dog'.
+ msg: Optional message to report on failure.
+ """
+ if isinstance(strings, (bytes, str)):
+ strings = (strings,)
+
+ current_index = 0
+ last_string = None
+ for string in strings:
+ index = target.find(str(string), current_index)
+ if index == -1 and current_index == 0:
+ self.fail("Did not find '%s' in '%s'" %
+ (string, target), msg)
+ elif index == -1:
+ self.fail("Did not find '%s' after '%s' in '%s'" %
+ (string, last_string, target), msg)
+ last_string = string
+ current_index = index
+
+ def assertContainsSubsequence(self, container, subsequence, msg=None):
+ """Asserts that "container" contains "subsequence" as a subsequence.
+
+ Asserts that "container" contains all the elements of "subsequence", in
+ order, but possibly with other elements interspersed. For example, [1, 2, 3]
+ is a subsequence of [0, 0, 1, 2, 0, 3, 0] but not of [0, 0, 1, 3, 0, 2, 0].
+
+ Args:
+ container: the list we're testing for subsequence inclusion.
+ subsequence: the list we hope will be a subsequence of container.
+ msg: Optional message to report on failure.
+ """
+ first_nonmatching = None
+ reversed_container = list(reversed(container))
+ subsequence = list(subsequence)
+
+ for e in subsequence:
+ if e not in reversed_container:
+ first_nonmatching = e
+ break
+ while e != reversed_container.pop():
+ pass
+
+ if first_nonmatching is not None:
+ self.fail('%s not a subsequence of %s. First non-matching element: %s' %
+ (subsequence, container, first_nonmatching), msg)
+
+ def assertContainsExactSubsequence(self, container, subsequence, msg=None):
+ """Asserts that "container" contains "subsequence" as an exact subsequence.
+
+ Asserts that "container" contains all the elements of "subsequence", in
+ order, and without other elements interspersed. For example, [1, 2, 3] is an
+ exact subsequence of [0, 0, 1, 2, 3, 0] but not of [0, 0, 1, 2, 0, 3, 0].
+
+ Args:
+ container: the list we're testing for subsequence inclusion.
+ subsequence: the list we hope will be an exact subsequence of container.
+ msg: Optional message to report on failure.
+ """
+ container = list(container)
+ subsequence = list(subsequence)
+ longest_match = 0
+
+ for start in range(1 + len(container) - len(subsequence)):
+ if longest_match == len(subsequence):
+ break
+ index = 0
+ while (index < len(subsequence) and
+ subsequence[index] == container[start + index]):
+ index += 1
+ longest_match = max(longest_match, index)
+
+ if longest_match < len(subsequence):
+ self.fail('%s not an exact subsequence of %s. '
+ 'Longest matching prefix: %s' %
+ (subsequence, container, subsequence[:longest_match]), msg)
+
+ def assertTotallyOrdered(self, *groups, **kwargs):
+ """Asserts that total ordering has been implemented correctly.
+
+ For example, say you have a class A that compares only on its attribute x.
+ Comparators other than ``__lt__`` are omitted for brevity::
+
+ class A(object):
+ def __init__(self, x, y):
+ self.x = x
+ self.y = y
+
+ def __hash__(self):
+ return hash(self.x)
+
+ def __lt__(self, other):
+ try:
+ return self.x < other.x
+ except AttributeError:
+ return NotImplemented
+
+ assertTotallyOrdered will check that instances can be ordered correctly.
+ For example::
+
+ self.assertTotallyOrdered(
+ [None], # None should come before everything else.
+ [1], # Integers sort earlier.
+ [A(1, 'a')],
+ [A(2, 'b')], # 2 is after 1.
+ [A(3, 'c'), A(3, 'd')], # The second argument is irrelevant.
+ [A(4, 'z')],
+ ['foo']) # Strings sort last.
+
+ Args:
+ *groups: A list of groups of elements. Each group of elements is a list
+ of objects that are equal. The elements in each group must be less
+ than the elements in the group after it. For example, these groups are
+ totally ordered: ``[None]``, ``[1]``, ``[2, 2]``, ``[3]``.
+ **kwargs: optional msg keyword argument can be passed.
+ """
+
+ def CheckOrder(small, big):
+ """Ensures small is ordered before big."""
+ self.assertFalse(small == big,
+ self._formatMessage(msg, '%r unexpectedly equals %r' %
+ (small, big)))
+ self.assertTrue(small != big,
+ self._formatMessage(msg, '%r unexpectedly equals %r' %
+ (small, big)))
+ self.assertLess(small, big, msg)
+ self.assertFalse(big < small,
+ self._formatMessage(msg,
+ '%r unexpectedly less than %r' %
+ (big, small)))
+ self.assertLessEqual(small, big, msg)
+ self.assertFalse(big <= small, self._formatMessage(
+ '%r unexpectedly less than or equal to %r' % (big, small), msg
+ ))
+ self.assertGreater(big, small, msg)
+ self.assertFalse(small > big,
+ self._formatMessage(msg,
+ '%r unexpectedly greater than %r' %
+ (small, big)))
+ self.assertGreaterEqual(big, small)
+ self.assertFalse(small >= big, self._formatMessage(
+ msg,
+ '%r unexpectedly greater than or equal to %r' % (small, big)))
+
+ def CheckEqual(a, b):
+ """Ensures that a and b are equal."""
+ self.assertEqual(a, b, msg)
+ self.assertFalse(a != b,
+ self._formatMessage(msg, '%r unexpectedly unequals %r' %
+ (a, b)))
+
+ # Objects that compare equal must hash to the same value, but this only
+ # applies if both objects are hashable.
+ if (isinstance(a, abc.Hashable) and
+ isinstance(b, abc.Hashable)):
+ self.assertEqual(
+ hash(a), hash(b),
+ self._formatMessage(
+ msg, 'hash %d of %r unexpectedly not equal to hash %d of %r' %
+ (hash(a), a, hash(b), b)))
+
+ self.assertFalse(a < b,
+ self._formatMessage(msg,
+ '%r unexpectedly less than %r' %
+ (a, b)))
+ self.assertFalse(b < a,
+ self._formatMessage(msg,
+ '%r unexpectedly less than %r' %
+ (b, a)))
+ self.assertLessEqual(a, b, msg)
+ self.assertLessEqual(b, a, msg) # pylint: disable=arguments-out-of-order
+ self.assertFalse(a > b,
+ self._formatMessage(msg,
+ '%r unexpectedly greater than %r' %
+ (a, b)))
+ self.assertFalse(b > a,
+ self._formatMessage(msg,
+ '%r unexpectedly greater than %r' %
+ (b, a)))
+ self.assertGreaterEqual(a, b, msg)
+ self.assertGreaterEqual(b, a, msg) # pylint: disable=arguments-out-of-order
+
+ msg = kwargs.get('msg')
+
+ # For every combination of elements, check the order of every pair of
+ # elements.
+ for elements in itertools.product(*groups):
+ elements = list(elements)
+ for index, small in enumerate(elements[:-1]):
+ for big in elements[index + 1:]:
+ CheckOrder(small, big)
+
+ # Check that every element in each group is equal.
+ for group in groups:
+ for a in group:
+ CheckEqual(a, a)
+ for a, b in itertools.product(group, group):
+ CheckEqual(a, b)
+
+ def assertDictContainsSubset(
+ self,
+ subset: abc.Mapping[Any, Any],
+ dictionary: abc.Mapping[Any, Any],
+ msg=None,
+ ):
+ """Raises AssertionError if "dictionary" is not a superset of "subset".
+
+ Args:
+ subset: A dict, the expected subset of the "dictionary".
+ dictionary: A dict, the actual value.
+ msg: An optional str, the associated message.
+
+ Raises:
+ AssertionError: if "dictionary" is not a superset of "subset".
+ """
+ self.assertDictEqual({**dictionary}, {**dictionary, **subset}, msg)
+
+ def assertDictEqual(self, a, b, msg=None):
+ """Raises AssertionError if a and b are not equal dictionaries.
+
+ Args:
+ a: A dict, the expected value.
+ b: A dict, the actual value.
+ msg: An optional str, the associated message.
+
+ Raises:
+ AssertionError: if the dictionaries are not equal.
+ """
+ self.assertMappingEqual(a, b, msg, mapping_type=dict)
+
+ def assertDictAlmostEqual(
+ self,
+ a,
+ b,
+ places=None,
+ msg=None,
+ delta=None,
+ ):
+ """Raises AssertionError if a and b are not equal or almost equal dicts.
+
+ This is like assertDictEqual, except for float values which are compared
+ using assertAlmostEqual. Almost equality is determined for float values by:
+ - have numeric difference less than the given delta,
+ or
+ - equal if rounded to the given number of decimal places after the decimal
+ point (default 7).
+
+ Args:
+ a: A dict, the expected value.
+ b: A dict, the actual value.
+ places: The number of decimal places to compare for floats.
+ msg: An optional str, the associated message.
+ delta: The OK difference between compared values for floats.
+
+ Raises:
+ AssertionError: if the dictionaries are not equal or almost equal.
+ ValueError: if both places and delta are specified.
+ """
+
+ # Almost equality with preset places and delta.
+ def almost_equal_compare(a_value, b_value):
+ if isinstance(a_value, numbers.Number) and isinstance(
+ b_value, numbers.Number
+ ):
+ try:
+ # assertAlmostEqual should be called with at most one of `places`
+ # and `delta`. However, it's okay for assertMappingEqual to pass
+ # both because we want the latter to fail if the former does.
+ # pytype: disable=wrong-keyword-args
+ self.assertAlmostEqual(
+ a_value,
+ b_value,
+ places=places,
+ delta=delta,
+ )
+ # pytype: enable=wrong-keyword-args
+ except self.failureException as err:
+ return False, err
+ return True, None
+ else:
+ # Fall back to regular equality check if the values are not numbers.
+ try:
+ self.assertEqual(a_value, b_value)
+ except self.failureException as err:
+ return False, err
+ return True, None
+
+ if delta is not None and places is not None:
+ raise ValueError('specify delta or places not both\n')
+
+ self.assertMappingEqual(
+ a,
+ b,
+ msg,
+ mapping_type=dict,
+ check_values_equality=almost_equal_compare,
+ )
+
+ def assertMappingEqual(
+ self,
+ a,
+ b,
+ msg=None,
+ mapping_type=abc.Mapping,
+ check_values_equality=lambda x, y: (x == y, None),
+ ):
+ """Raises AssertionError if a and b differ in keys or values.
+
+ Key sets must be exectly the same, the corresponding values should satisfy
+ the provided equality function.
+
+ Args:
+ a: A mapping, the expected value.
+ b: A mapping, the actual value.
+ msg: An optional str, the associated message.
+ mapping_type: The expected type of the mappings.
+ check_values_equality: A function that takes two values and returns a
+ tuple of (bool, BaseException), where the bool is True if the values are
+ equal and the BaseException is an optional exception occured during the
+ equality check.
+
+ Raises:
+ AssertionError: if the dictionaries are not equal.
+ """
+
+ if not isinstance(a, mapping_type):
+ self.fail(
+ f'a should be a {mapping_type.__name__}, found type:'
+ f' {type(a).__name__}',
+ msg,
+ )
+ if not isinstance(b, mapping_type):
+ self.fail(
+ f'b should be a {mapping_type.__name__}, found type:'
+ f' {type(b).__name__}',
+ msg,
+ )
+ if a == b:
+ return
+
+ def Sorted(list_of_items):
+ try:
+ return sorted(list_of_items) # In 3.3, unordered are possible.
+ except TypeError:
+ return list_of_items
+
+ a_items = Sorted(list(a.items()))
+ b_items = Sorted(list(b.items()))
+
+ unexpected = []
+ missing = []
+ different = []
+
+ # The standard library default output confounds lexical difference with
+ # value difference; treat them separately.
+ for a_key, a_value in a_items:
+ if a_key not in b:
+ missing.append((a_key, a_value))
+ continue
+ b_value = b[a_key]
+ is_equal, err = check_values_equality(a_value, b_value)
+ if not is_equal:
+ different.append((a_key, a_value, b_value, err))
+
+ for b_key, b_value in b_items:
+ if b_key not in a:
+ unexpected.append((b_key, b_value))
+
+ # If all difference buckets are empty, then mappings are considered equal.
+ if not unexpected and not different and not missing:
+ return
+
+ safe_repr = unittest.util.safe_repr # pytype: disable=module-attr
+
+ def Repr(dikt):
+ """Deterministic repr for dict."""
+ # Sort the entries based on their repr, not based on their sort order,
+ # which will be non-deterministic across executions, for many types.
+ entries = sorted((safe_repr(k), safe_repr(v)) for k, v in dikt.items())
+ return '{' + ', '.join(f'{k}: {v}' for k, v in entries) + '}'
+
+ message = [f'{Repr(a)} != {Repr(b)}{"("+msg+")" if msg else ""}']
+
+ if unexpected:
+ message.append(
+ 'Unexpected, but present entries:\n'
+ + ''.join(f'{safe_repr(k)}: {safe_repr(v)}\n' for k, v in unexpected)
+ )
+
+ if different:
+ message.append(
+ 'repr() of differing entries:\n'
+ + ''.join(
+ f'{safe_repr(k)}: '
+ f'{err if err else f"{safe_repr(a_value)} != {safe_repr(b_value)}"}\n'
+ for k, a_value, b_value, err in different
+ )
+ )
+
+ if missing:
+ message.append(
+ 'Missing entries:\n'
+ + ''.join(f'{safe_repr(k)}: {safe_repr(v)}\n' for k, v in missing)
+ )
+
+ raise self.failureException('\n'.join(message))
+
+ def assertDataclassEqual(self, first, second, msg=None):
+ """Asserts two dataclasses are equal with more informative errors.
+
+ Arguments must both be dataclasses. This compares equality of individual
+ fields and takes care to not compare fields that are marked as
+ non-comparable. It gives per field differences, which are easier to parse
+ than the comparison of the string representations from assertEqual.
+
+ In cases where the dataclass has a custom __eq__, and it is defined in a
+ way that is inconsistent with equality of comparable fields, we raise an
+ exception without further trying to figure out how they are different.
+
+ Args:
+ first: A dataclass, the first value.
+ second: A dataclass, the second value.
+ msg: An optional str, the associated message.
+
+ Raises:
+ AssertionError: if the dataclasses are not equal.
+ """
+
+ if not dataclasses.is_dataclass(first) or isinstance(first, type):
+ raise self.failureException('First argument is not a dataclass instance.')
+ if not dataclasses.is_dataclass(second) or isinstance(second, type):
+ raise self.failureException(
+ 'Second argument is not a dataclass instance.'
+ )
+
+ if first == second:
+ return
+
+ if type(first) is not type(second):
+ self.fail(
+ 'Found different dataclass types: %s != %s'
+ % (type(first), type(second)),
+ msg,
+ )
+
+ # Make sure to skip fields that are marked compare=False.
+ different = [
+ (f.name, getattr(first, f.name), getattr(second, f.name))
+ for f in dataclasses.fields(first)
+ if f.compare and getattr(first, f.name) != getattr(second, f.name)
+ ]
+
+ safe_repr = unittest.util.safe_repr # pytype: disable=module-attr
+ message = ['%s != %s' % (safe_repr(first), safe_repr(second))]
+ if different:
+ message.append('Fields that differ:')
+ message.extend(
+ '%s: %s != %s' % (k, safe_repr(first_v), safe_repr(second_v))
+ for k, first_v, second_v in different
+ )
+ else:
+ message.append(
+ 'Cannot detect difference by examining the fields of the dataclass.'
+ )
+
+ self.fail('\n'.join(message), msg)
+
+ def assertUrlEqual(self, a, b, msg=None):
+ """Asserts that urls are equal, ignoring ordering of query params."""
+ parsed_a = parse.urlparse(a)
+ parsed_b = parse.urlparse(b)
+ self.assertEqual(parsed_a.scheme, parsed_b.scheme, msg)
+ self.assertEqual(parsed_a.netloc, parsed_b.netloc, msg)
+ self.assertEqual(parsed_a.path, parsed_b.path, msg)
+ self.assertEqual(parsed_a.fragment, parsed_b.fragment, msg)
+ self.assertEqual(sorted(parsed_a.params.split(';')),
+ sorted(parsed_b.params.split(';')), msg)
+ self.assertDictEqual(
+ parse.parse_qs(parsed_a.query, keep_blank_values=True),
+ parse.parse_qs(parsed_b.query, keep_blank_values=True), msg)
+
+ def assertSameStructure(self, a, b, aname='a', bname='b', msg=None):
+ """Asserts that two values contain the same structural content.
+
+ The two arguments should be data trees consisting of trees of dicts and
+ lists. They will be deeply compared by walking into the contents of dicts
+ and lists; other items will be compared using the == operator.
+ If the two structures differ in content, the failure message will indicate
+ the location within the structures where the first difference is found.
+ This may be helpful when comparing large structures.
+
+ Mixed Sequence and Set types are supported. Mixed Mapping types are
+ supported, but the order of the keys will not be considered in the
+ comparison.
+
+ Args:
+ a: The first structure to compare.
+ b: The second structure to compare.
+ aname: Variable name to use for the first structure in assertion messages.
+ bname: Variable name to use for the second structure.
+ msg: Additional text to include in the failure message.
+ """
+
+ # Accumulate all the problems found so we can report all of them at once
+ # rather than just stopping at the first
+ problems = []
+
+ _walk_structure_for_problems(a, b, aname, bname, problems,
+ self.assertEqual, self.failureException)
+
+ # Avoid spamming the user toooo much
+ if self.maxDiff is not None:
+ max_problems_to_show = self.maxDiff // 80
+ if len(problems) > max_problems_to_show:
+ problems = problems[0:max_problems_to_show-1] + ['...']
+
+ if problems:
+ self.fail('; '.join(problems), msg)
+
+ def assertJsonEqual(self, first, second, msg=None):
+ """Asserts that the JSON objects defined in two strings are equal.
+
+ A summary of the differences will be included in the failure message
+ using assertSameStructure.
+
+ Args:
+ first: A string containing JSON to decode and compare to second.
+ second: A string containing JSON to decode and compare to first.
+ msg: Additional text to include in the failure message.
+ """
+ try:
+ first_structured = json.loads(first)
+ except ValueError as e:
+ raise ValueError(self._formatMessage(
+ msg,
+ 'could not decode first JSON value %s: %s' % (first, e)))
+
+ try:
+ second_structured = json.loads(second)
+ except ValueError as e:
+ raise ValueError(self._formatMessage(
+ msg,
+ 'could not decode second JSON value %s: %s' % (second, e)))
+
+ self.assertSameStructure(first_structured, second_structured,
+ aname='first', bname='second', msg=msg)
+
+ def _getAssertEqualityFunc(
+ self, first: Any, second: Any
+ ) -> abc.Callable[..., None]:
+ try:
+ return super()._getAssertEqualityFunc(first, second)
+ except AttributeError:
+ # This is a workaround if unittest.TestCase.__init__ was never run.
+ # It usually means that somebody created a subclass just for the
+ # assertions and has overridden __init__. "assertTrue" is a safe
+ # value that will not make __init__ raise a ValueError.
+ test_method = getattr(self, '_testMethodName', 'assertTrue')
+ super().__init__(test_method)
+
+ return super()._getAssertEqualityFunc(first, second)
+
+ def fail(self, msg=None, user_msg=None) -> NoReturn:
+ """Fail immediately with the given standard message and user message."""
+ super().fail(self._formatMessage(user_msg, msg))
+
+
+def _sorted_list_difference(
+ expected: list[_T], actual: list[_T]
+) -> tuple[list[_T], list[_T]]:
+ """Finds elements in only one or the other of two, sorted input lists.
+
+ Returns a two-element tuple of lists. The first list contains those
+ elements in the "expected" list but not in the "actual" list, and the
+ second contains those elements in the "actual" list but not in the
+ "expected" list. Duplicate elements in either input list are ignored.
+
+ Args:
+ expected: The list we expected.
+ actual: The list we actually got.
+ Returns:
+ (missing, unexpected)
+ missing: items in expected that are not in actual.
+ unexpected: items in actual that are not in expected.
+ """
+ i = j = 0
+ missing = []
+ unexpected = []
+ while True:
+ try:
+ e = expected[i]
+ a = actual[j]
+ if e < a: # type: ignore[operator]
+ missing.append(e)
+ i += 1
+ while expected[i] == e:
+ i += 1
+ elif e > a: # type: ignore[operator]
+ unexpected.append(a)
+ j += 1
+ while actual[j] == a:
+ j += 1
+ else:
+ i += 1
+ try:
+ while expected[i] == e:
+ i += 1
+ finally:
+ j += 1
+ while actual[j] == a:
+ j += 1
+ except IndexError:
+ missing.extend(expected[i:])
+ unexpected.extend(actual[j:])
+ break
+ return missing, unexpected
+
+
+def _are_both_of_integer_type(a: object, b: object) -> bool:
+ return isinstance(a, int) and isinstance(b, int)
+
+
+def _are_both_of_sequence_type(a: object, b: object) -> bool:
+ return isinstance(a, abc.Sequence) and isinstance(
+ b, abc.Sequence) and not isinstance(
+ a, _TEXT_OR_BINARY_TYPES) and not isinstance(b, _TEXT_OR_BINARY_TYPES)
+
+
+def _are_both_of_set_type(a: object, b: object) -> bool:
+ return isinstance(a, abc.Set) and isinstance(b, abc.Set)
+
+
+def _are_both_of_mapping_type(a: object, b: object) -> bool:
+ return isinstance(a, abc.Mapping) and isinstance(
+ b, abc.Mapping)
+
+
+def _walk_structure_for_problems(
+ a, b, aname, bname, problem_list, leaf_assert_equal_func, failure_exception
+):
+ """The recursive comparison behind assertSameStructure."""
+ if type(a) != type(b) and not ( # pylint: disable=unidiomatic-typecheck
+ _are_both_of_integer_type(a, b) or _are_both_of_sequence_type(a, b) or
+ _are_both_of_set_type(a, b) or _are_both_of_mapping_type(a, b)):
+ # We do not distinguish between int and long types as 99.99% of Python 2
+ # code should never care. They collapse into a single type in Python 3.
+ problem_list.append('%s is a %r but %s is a %r' %
+ (aname, type(a), bname, type(b)))
+ # If they have different types there's no point continuing
+ return
+
+ if isinstance(a, abc.Set):
+ for k in a:
+ if k not in b:
+ problem_list.append(
+ '%s has %r but %s does not' % (aname, k, bname))
+ for k in b:
+ if k not in a:
+ problem_list.append('%s lacks %r but %s has it' % (aname, k, bname))
+
+ # NOTE: a or b could be a defaultdict, so we must take care that the traversal
+ # doesn't modify the data.
+ elif isinstance(a, abc.Mapping):
+ for k in a:
+ if k in b:
+ _walk_structure_for_problems(
+ a[k], b[k], '%s[%r]' % (aname, k), '%s[%r]' % (bname, k),
+ problem_list, leaf_assert_equal_func, failure_exception)
+ else:
+ problem_list.append(
+ "%s has [%r] with value %r but it's missing in %s" %
+ (aname, k, a[k], bname))
+ for k in b:
+ if k not in a:
+ problem_list.append(
+ '%s lacks [%r] but %s has it with value %r' %
+ (aname, k, bname, b[k]))
+
+ # Strings/bytes are Sequences but we'll just do those with regular !=
+ elif (isinstance(a, abc.Sequence) and
+ not isinstance(a, _TEXT_OR_BINARY_TYPES)):
+ minlen = min(len(a), len(b))
+ for i in range(minlen):
+ _walk_structure_for_problems(
+ a[i], b[i], '%s[%d]' % (aname, i), '%s[%d]' % (bname, i),
+ problem_list, leaf_assert_equal_func, failure_exception)
+ for i in range(minlen, len(a)):
+ problem_list.append('%s has [%i] with value %r but %s does not' %
+ (aname, i, a[i], bname))
+ for i in range(minlen, len(b)):
+ problem_list.append('%s lacks [%i] but %s has it with value %r' %
+ (aname, i, bname, b[i]))
+
+ else:
+ try:
+ leaf_assert_equal_func(a, b)
+ except failure_exception:
+ problem_list.append('%s is %r but %s is %r' % (aname, a, bname, b))
+
+
+def get_command_string(command):
+ """Returns an escaped string that can be used as a shell command.
+
+ Args:
+ command: List or string representing the command to run.
+ Returns:
+ A string suitable for use as a shell command.
+ """
+ if isinstance(command, str):
+ return command
+ else:
+ if os.name == 'nt':
+ return ' '.join(command)
+ else:
+ # The following is identical to Python 3's shlex.quote function.
+ command_string = ''
+ for word in command:
+ # Single quote word, and replace each ' in word with '"'"'
+ command_string += "'" + word.replace("'", "'\"'\"'") + "' "
+ return command_string[:-1]
+
+
+def get_command_stderr(command, env=None, close_fds=True):
+ """Runs the given shell command and returns a tuple.
+
+ Args:
+ command: List or string representing the command to run.
+ env: Dictionary of environment variable settings. If None, no environment
+ variables will be set for the child process. This is to make tests
+ more hermetic. NOTE: this behavior is different than the standard
+ subprocess module.
+ close_fds: Whether or not to close all open fd's in the child after forking.
+ On Windows, this is ignored and close_fds is always False.
+
+ Returns:
+ Tuple of (exit status, text printed to stdout and stderr by the command).
+ """
+ if env is None: env = {}
+ if os.name == 'nt':
+ # Windows does not support setting close_fds to True while also redirecting
+ # standard handles.
+ close_fds = False
+
+ use_shell = isinstance(command, str)
+ # Pass the shell command as stdin to /bin/sh rather than using Python's
+ # behavior of passing it in as a command line argument. That can save us when
+ # the shell command exceeds the maximum command line length but the actual
+ # individual process invocations within it don't.
+ if os.name != 'nt' and use_shell:
+ stdin_input = command.encode()
+ command = ['/bin/sh']
+ use_shell = False
+ else:
+ stdin_input = None
+
+ result = subprocess.run(
+ command,
+ close_fds=close_fds,
+ env=env,
+ shell=use_shell,
+ input=stdin_input,
+ stderr=subprocess.STDOUT,
+ stdout=subprocess.PIPE,
+ check=False,
+ )
+ return (result.returncode, result.stdout)
+
+
+def _quote_long_string(s: str | bytes | bytearray) -> str:
+ """Quotes a potentially multi-line string to make the start and end obvious.
+
+ Args:
+ s: A string.
+
+ Returns:
+ The quoted string.
+ """
+ if isinstance(s, (bytes, bytearray)):
+ try:
+ s = s.decode('utf-8')
+ except UnicodeDecodeError:
+ s = str(s)
+ return ('8<-----------\n' +
+ s + '\n' +
+ '----------->8\n')
+
+
+def print_python_version() -> None:
+ # Having this in the test output logs by default helps debugging when all
+ # you've got is the log and no other idea of which Python was used.
+ sys.stderr.write('Running tests under Python {0[0]}.{0[1]}.{0[2]}: '
+ '{1}\n'.format(
+ sys.version_info,
+ sys.executable if sys.executable else 'embedded.'))
+
+
+def main(*args: str, **kwargs: Any) -> None:
+ """Executes a set of Python unit tests.
+
+ Usually this function is called without arguments, so the
+ unittest.TestProgram instance will get created with the default settings,
+ so it will run all test methods of all TestCase classes in the ``__main__``
+ module.
+
+ Args:
+ *args: Positional arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ **kwargs: Keyword arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ """
+ print_python_version()
+ _run_in_app(run_tests, args, kwargs)
+
+
+def _is_in_app_main() -> bool:
+ """Returns True iff app.run is active."""
+ f = sys._getframe().f_back # pylint: disable=protected-access
+ while f:
+ if f.f_code == app.run.__code__:
+ return True
+ f = f.f_back
+ return False
+
+
+def _register_sigterm_with_faulthandler() -> None:
+ """Have faulthandler dump stacks on SIGTERM. Useful to diagnose timeouts."""
+ if getattr(faulthandler, 'register', None):
+ # faulthandler.register is not available on Windows.
+ # faulthandler.enable() is already called by app.run.
+ try:
+ faulthandler.register(signal.SIGTERM, chain=True) # pytype: disable=module-attr
+ except Exception as e: # pylint: disable=broad-except
+ sys.stderr.write('faulthandler.register(SIGTERM) failed '
+ '%r; ignoring.\n' % e)
+
+
+def _run_in_app(
+ function: abc.Callable[..., None],
+ args: abc.Sequence[str],
+ kwargs: abc.Mapping[str, Any],
+) -> None:
+ """Executes a set of Python unit tests, ensuring app.run.
+
+ This is a private function, users should call absltest.main().
+
+ _run_in_app calculates argv to be the command-line arguments of this program
+ (without the flags), sets the default of FLAGS.alsologtostderr to True,
+ then it calls function(argv, args, kwargs), making sure that `function'
+ will get called within app.run(). _run_in_app does this by checking whether
+ it is called by app.run(), or by calling app.run() explicitly.
+
+ The reason why app.run has to be ensured is to make sure that
+ flags are parsed and stripped properly, and other initializations done by
+ the app module are also carried out, no matter if absltest.run() is called
+ from within or outside app.run().
+
+ If _run_in_app is called from within app.run(), then it will reparse
+ sys.argv and pass the result without command-line flags into the argv
+ argument of `function'. The reason why this parsing is needed is that
+ __main__.main() calls absltest.main() without passing its argv. So the
+ only way _run_in_app could get to know the argv without the flags is that
+ it reparses sys.argv.
+
+ _run_in_app changes the default of FLAGS.alsologtostderr to True so that the
+ test program's stderr will contain all the log messages unless otherwise
+ specified on the command-line. This overrides any explicit assignment to
+ FLAGS.alsologtostderr by the test program prior to the call to _run_in_app()
+ (e.g. in __main__.main).
+
+ Please note that _run_in_app (and the function it calls) is allowed to make
+ changes to kwargs.
+
+ Args:
+ function: absltest.run_tests or a similar function. It will be called as
+ function(argv, args, kwargs) where argv is a list containing the
+ elements of sys.argv without the command-line flags.
+ args: Positional arguments passed through to unittest.TestProgram.__init__.
+ kwargs: Keyword arguments passed through to unittest.TestProgram.__init__.
+ """
+ if _is_in_app_main():
+ _register_sigterm_with_faulthandler()
+
+ # Change the default of alsologtostderr from False to True, so the test
+ # programs's stderr will contain all the log messages.
+ # If --alsologtostderr=false is specified in the command-line, or user
+ # has called FLAGS.alsologtostderr = False before, then the value is kept
+ # False.
+ FLAGS.set_default('alsologtostderr', True)
+
+ # Here we only want to get the `argv` without the flags. To avoid any
+ # side effects of parsing flags, we temporarily stub out the `parse` method
+ stored_parse_methods = {}
+ noop_parse = lambda _: None
+ for name in FLAGS:
+ # Avoid any side effects of parsing flags.
+ stored_parse_methods[name] = FLAGS[name].parse
+ # This must be a separate loop since multiple flag names (short_name=) can
+ # point to the same flag object.
+ for name in FLAGS:
+ FLAGS[name].parse = noop_parse # type: ignore[method-assign]
+ try:
+ argv = FLAGS(sys.argv)
+ finally:
+ for name in FLAGS:
+ FLAGS[name].parse = stored_parse_methods[name] # type: ignore[method-assign]
+ sys.stdout.flush()
+
+ function(argv, args, kwargs)
+ else:
+ # Send logging to stderr. Use --alsologtostderr instead of --logtostderr
+ # in case tests are reading their own logs.
+ FLAGS.set_default('alsologtostderr', True)
+
+ def main_function(argv):
+ _register_sigterm_with_faulthandler()
+ function(argv, args, kwargs)
+
+ app.run(main=main_function)
+
+
+def _is_suspicious_attribute(
+ testCaseClass: type[unittest.TestCase], name: str
+) -> bool:
+ """Returns True if an attribute is a method named like a test method."""
+ if name.startswith('Test') and len(name) > 4 and name[4].isupper():
+ attr = getattr(testCaseClass, name)
+ if inspect.isfunction(attr) or inspect.ismethod(attr):
+ args = inspect.getfullargspec(attr)
+ return (len(args.args) == 1 and args.args[0] == 'self' and
+ args.varargs is None and args.varkw is None and
+ not args.kwonlyargs)
+ return False
+
+
+def skipThisClass(
+ reason: str,
+) -> abc.Callable[[type[_T]], type[_T]]:
+ """Skip tests in the decorated TestCase, but not any of its subclasses.
+
+ This decorator indicates that this class should skip all its tests, but not
+ any of its subclasses. Useful for if you want to share testMethod or setUp
+ implementations between a number of concrete testcase classes.
+
+ Example usage, showing how you can share some common test methods between
+ subclasses. In this example, only ``BaseTest`` will be marked as skipped, and
+ not RealTest or SecondRealTest::
+
+ @absltest.skipThisClass("Shared functionality")
+ class BaseTest(absltest.TestCase):
+ def test_simple_functionality(self):
+ self.assertEqual(self.system_under_test.method(), 1)
+
+ class RealTest(BaseTest):
+ def setUp(self):
+ super().setUp()
+ self.system_under_test = MakeSystem(argument)
+
+ def test_specific_behavior(self):
+ ...
+
+ class SecondRealTest(BaseTest):
+ def setUp(self):
+ super().setUp()
+ self.system_under_test = MakeSystem(other_arguments)
+
+ def test_other_behavior(self):
+ ...
+
+ Args:
+ reason: The reason we have a skip in place. For instance: 'shared test
+ methods' or 'shared assertion methods'.
+
+ Returns:
+ Decorator function that will cause a class to be skipped.
+ """
+ if isinstance(reason, type):
+ raise TypeError(f'Got {reason!r}, expected reason as string')
+
+ def _skip_class(test_case_class):
+ if not issubclass(test_case_class, unittest.TestCase):
+ raise TypeError(
+ f'Decorating {test_case_class!r}, expected TestCase subclass'
+ )
+
+ # Only shadow the setUpClass method if it is directly defined. If it is
+ # in the parent class we invoke it via a super() call instead of holding
+ # a reference to it.
+ shadowed_setupclass = test_case_class.__dict__.get('setUpClass', None)
+
+ @classmethod
+ def replacement_setupclass(cls, *args, **kwargs):
+ # Skip this class if it is the one that was decorated with @skipThisClass
+ if cls is test_case_class:
+ raise SkipTest(reason)
+ if shadowed_setupclass:
+ # Pass along `cls` so the MRO chain doesn't break.
+ # The original method is a `classmethod` descriptor, which can't
+ # be directly called, but `__func__` has the underlying function.
+ return shadowed_setupclass.__func__(cls, *args, **kwargs)
+ else:
+ # Because there's no setUpClass() defined directly on test_case_class,
+ # we call super() ourselves to continue execution of the inheritance
+ # chain.
+ return super(test_case_class, cls).setUpClass(*args, **kwargs)
+
+ test_case_class.setUpClass = replacement_setupclass
+ return test_case_class
+
+ return _skip_class
+
+
+class TestLoader(unittest.TestLoader):
+ """A test loader which supports common test features.
+
+ Supported features include:
+ * Banning untested methods with test-like names: methods attached to this
+ testCase with names starting with `Test` are ignored by the test runner,
+ and often represent mistakenly-omitted test cases. This loader will raise
+ a TypeError when attempting to load a TestCase with such methods.
+ * Randomization of test case execution order (optional).
+ """
+
+ _ERROR_MSG = textwrap.dedent("""Method '%s' is named like a test case but
+ is not one. This is often a bug. If you want it to be a test method,
+ name it with 'test' in lowercase. If not, rename the method to not begin
+ with 'Test'.""")
+
+ def __init__(self, *args, **kwds):
+ super().__init__(*args, **kwds)
+ seed = _get_default_randomize_ordering_seed()
+ if seed:
+ self._randomize_ordering_seed = seed
+ self._random = random.Random(self._randomize_ordering_seed)
+ else:
+ self._randomize_ordering_seed = None
+ self._random = None
+
+ def getTestCaseNames(self, testCaseClass): # pylint:disable=invalid-name
+ """Validates and returns a (possibly randomized) list of test case names."""
+ for name in dir(testCaseClass):
+ if _is_suspicious_attribute(testCaseClass, name):
+ raise TypeError(TestLoader._ERROR_MSG % name)
+ names = list(super().getTestCaseNames(testCaseClass))
+ if self._randomize_ordering_seed is not None and self._random is not None:
+ logging.info(
+ 'Randomizing test order with seed: %d', self._randomize_ordering_seed)
+ logging.info(
+ 'To reproduce this order, re-run with '
+ '--test_randomize_ordering_seed=%d', self._randomize_ordering_seed)
+ self._random.shuffle(names)
+ return names
+
+ def shardTestCaseNames(
+ self,
+ iterator: abc.Iterator[Any],
+ ordered_names: abc.Sequence[str],
+ shard_index: int,
+ ) -> abc.Sequence[str]:
+ """Filters and returns test case names for a specific shard.
+
+ This method is intended to be used in conjunction with test sharding
+ (e.g., when running tests on a distributed system or when running tests
+ with bazel's test sharding feature). It will return a subset of the
+ input test case names, based on the shard index and total shard count.
+
+ Args:
+ iterator: An iterator over the shards, where each iteration returns the
+ next shard index.
+ ordered_names: A sequence of test case names.
+ shard_index: The index of the current shard.
+
+ Returns:
+ A sequence of test case names for the current shard.
+ """
+ filtered_names = []
+ # We need to sort the list of tests in order to determine which tests this
+ # shard is responsible for; however, it's important to preserve the order
+ # returned by the base loader, e.g. in the case of randomized test ordering.
+ for testcase in sorted(ordered_names):
+ bucket = next(iterator)
+ if bucket == shard_index:
+ filtered_names.append(testcase)
+ return [x for x in ordered_names if x in filtered_names]
+
+
+def get_default_xml_output_filename() -> str | None:
+ if os.environ.get('XML_OUTPUT_FILE'):
+ return os.environ['XML_OUTPUT_FILE']
+ elif os.environ.get('RUNNING_UNDER_TEST_DAEMON'):
+ return os.path.join(os.path.dirname(TEST_TMPDIR.value), 'test_detail.xml')
+ elif os.environ.get('TEST_XMLOUTPUTDIR'):
+ return os.path.join(
+ os.environ['TEST_XMLOUTPUTDIR'],
+ os.path.splitext(os.path.basename(sys.argv[0]))[0] + '.xml')
+ return None
+
+
+def _setup_filtering(argv: abc.MutableSequence[str]) -> bool:
+ """Implements the bazel test filtering protocol.
+
+ The following environment variable is used in this method:
+
+ TESTBRIDGE_TEST_ONLY: string, if set, is forwarded to the unittest
+ framework to use as a test filter. Its value is split with shlex, then:
+ 1. On Python 3.6 and before, split values are passed as positional
+ arguments on argv.
+ 2. On Python 3.7+, split values are passed to unittest's `-k` flag. Tests
+ are matched by glob patterns or substring. See
+ https://docs.python.org/3/library/unittest.html#cmdoption-unittest-k
+
+ Args:
+ argv: the argv to mutate in-place.
+
+ Returns:
+ Whether test filtering is requested.
+ """
+ test_filter = os.environ.get('TESTBRIDGE_TEST_ONLY')
+ if argv is None or not test_filter:
+ return False
+
+ filters = ['-k=' + test_filter for test_filter in shlex.split(test_filter)]
+
+ argv[1:1] = filters
+ return True
+
+
+def _setup_test_runner_fail_fast(argv: abc.MutableSequence[str]) -> None:
+ """Implements the bazel test fail fast protocol.
+
+ The following environment variable is used in this method:
+
+ TESTBRIDGE_TEST_RUNNER_FAIL_FAST=<1|0>
+
+ If set to 1, --failfast is passed to the unittest framework to return upon
+ first failure.
+
+ Args:
+ argv: the argv to mutate in-place.
+ """
+
+ if argv is None:
+ return
+
+ if os.environ.get('TESTBRIDGE_TEST_RUNNER_FAIL_FAST') != '1':
+ return
+
+ argv[1:1] = ['--failfast']
+
+
+def _setup_sharding(
+ custom_loader: unittest.TestLoader | None = None,
+) -> tuple[unittest.TestLoader, int | None]:
+ """Implements the bazel sharding protocol.
+
+ The following environment variables are used in this method:
+
+ TEST_SHARD_STATUS_FILE: string, if set, points to a file. We write a blank
+ file to tell the test runner that this test implements the test sharding
+ protocol.
+
+ TEST_TOTAL_SHARDS: int, if set, sharding is requested.
+
+ TEST_SHARD_INDEX: int, must be set if TEST_TOTAL_SHARDS is set. Specifies
+ the shard index for this instance of the test process. Must satisfy:
+ 0 <= TEST_SHARD_INDEX < TEST_TOTAL_SHARDS.
+
+ Args:
+ custom_loader: A TestLoader to be made sharded.
+
+ Returns:
+ A tuple of ``(test_loader, shard_index)``. ``test_loader`` is for
+ shard-filtering or the standard test loader depending on the sharding
+ environment variables. ``shard_index`` is the shard index, or ``None`` when
+ sharding is not used.
+ """
+
+ # It may be useful to write the shard file even if the other sharding
+ # environment variables are not set. Test runners may use this functionality
+ # to query whether a test binary implements the test sharding protocol.
+ if 'TEST_SHARD_STATUS_FILE' in os.environ:
+ try:
+ with open(os.environ['TEST_SHARD_STATUS_FILE'], 'w') as f:
+ f.write('')
+ except OSError:
+ sys.stderr.write('Error opening TEST_SHARD_STATUS_FILE (%s). Exiting.'
+ % os.environ['TEST_SHARD_STATUS_FILE'])
+ sys.exit(1)
+
+ base_loader = custom_loader or TestLoader()
+ if 'TEST_TOTAL_SHARDS' not in os.environ:
+ # Not using sharding, use the expected test loader.
+ return base_loader, None
+
+ total_shards = int(os.environ['TEST_TOTAL_SHARDS'])
+ shard_index = int(os.environ['TEST_SHARD_INDEX'])
+
+ if shard_index < 0 or shard_index >= total_shards:
+ sys.stderr.write('ERROR: Bad sharding values. index=%d, total=%d\n' %
+ (shard_index, total_shards))
+ sys.exit(1)
+
+ # Replace the original getTestCaseNames with one that returns
+ # the test case names for this shard.
+ delegate_get_names = base_loader.getTestCaseNames
+
+ bucket_iterator = itertools.cycle(range(total_shards))
+
+ def getSharedTestCaseNames(testCaseClass):
+ has_shard_test_case_names = hasattr(base_loader, 'shardTestCaseNames')
+ if has_shard_test_case_names:
+ sharder = getattr(base_loader, 'shardTestCaseNames')
+ else:
+ sharder = lambda *args: TestLoader.shardTestCaseNames(base_loader, *args)
+
+ names = sharder(
+ bucket_iterator, delegate_get_names(testCaseClass), shard_index
+ )
+ return names
+
+ base_loader.getTestCaseNames = getSharedTestCaseNames # type: ignore[method-assign]
+ return base_loader, shard_index
+
+
+def _run_and_get_tests_result(
+ argv: abc.MutableSequence[str],
+ args: abc.Sequence[Any],
+ kwargs: abc.MutableMapping[str, Any],
+ xml_test_runner_class: type[unittest.TextTestRunner],
+) -> tuple[unittest.TestResult, bool]:
+ """Same as run_tests, but it doesn't exit.
+
+ Args:
+ argv: sys.argv with the command-line flags removed from the front, i.e. the
+ argv with which :func:`app.run()` has called
+ ``__main__.main``. It is passed to
+ ``unittest.TestProgram.__init__(argv=)``, which does its own flag parsing.
+ It is ignored if kwargs contains an argv entry.
+ args: Positional arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ kwargs: Keyword arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ xml_test_runner_class: The type of the test runner class.
+
+ Returns:
+ A tuple of ``(test_result, fail_when_no_tests_ran)``.
+ ``fail_when_no_tests_ran`` indicates whether the test should fail when
+ no tests ran.
+ """
+
+ # The entry from kwargs overrides argv.
+ argv = kwargs.pop('argv', argv)
+
+ if sys.version_info[:2] >= (3, 12):
+ # Python 3.12 unittest changed the behavior from PASS to FAIL in
+ # https://github.com/python/cpython/pull/102051. absltest follows this.
+ fail_when_no_tests_ran = True
+ else:
+ # Historically, absltest and unittest before Python 3.12 passes if no tests
+ # ran.
+ fail_when_no_tests_ran = False
+
+ # Set up test filtering if requested in environment.
+ if _setup_filtering(argv):
+ # When test filtering is requested, ideally we also want to fail when no
+ # tests ran. However, the test filters are usually done when running bazel.
+ # When you run multiple targets, e.g. `bazel test //my_dir/...
+ # --test_filter=MyTest`, you don't necessarily want individual tests to fail
+ # because no tests match in that particular target.
+ # Due to this use case, we don't fail when test filtering is requested via
+ # the environment variable from bazel.
+ fail_when_no_tests_ran = False
+
+ # Set up --failfast as requested in environment
+ _setup_test_runner_fail_fast(argv)
+
+ # Shard the (default or custom) loader if sharding is turned on.
+ kwargs['testLoader'], shard_index = _setup_sharding(
+ kwargs.get('testLoader', None)
+ )
+ if shard_index is not None and shard_index > 0:
+ # When sharding is requested, all the shards except the first one shall not
+ # fail when no tests ran. This happens when the shard count is greater than
+ # the test case count.
+ fail_when_no_tests_ran = False
+
+ # XML file name is based upon (sorted by priority):
+ # --xml_output_file flag, XML_OUTPUT_FILE variable,
+ # TEST_XMLOUTPUTDIR variable or RUNNING_UNDER_TEST_DAEMON variable.
+ if FLAGS.xml_output_file:
+ xml_output_file = FLAGS.xml_output_file
+ else:
+ xml_output_file = get_default_xml_output_filename()
+ if xml_output_file:
+ FLAGS.xml_output_file = xml_output_file # type: ignore[assignment]
+
+ xml_buffer = None
+ if xml_output_file:
+ xml_output_dir = os.path.dirname(xml_output_file)
+ if xml_output_dir and not os.path.isdir(xml_output_dir):
+ try:
+ os.makedirs(xml_output_dir)
+ except OSError as e:
+ # File exists error can occur with concurrent tests
+ if e.errno != errno.EEXIST:
+ raise
+ # Fail early if we can't write to the XML output file. This is so that we
+ # don't waste people's time running tests that will just fail anyways.
+ with _open(xml_output_file, 'w'):
+ pass
+
+ # We can reuse testRunner if it supports XML output (e. g. by inheriting
+ # from xml_reporter.TextAndXMLTestRunner). Otherwise we need to use
+ # xml_reporter.TextAndXMLTestRunner.
+ if (kwargs.get('testRunner') is not None
+ and not hasattr(kwargs['testRunner'], 'set_default_xml_stream')):
+ sys.stderr.write('WARNING: XML_OUTPUT_FILE or --xml_output_file setting '
+ 'overrides testRunner=%r setting (possibly from --pdb)'
+ % (kwargs['testRunner']))
+ # Passing a class object here allows TestProgram to initialize
+ # instances based on its kwargs and/or parsed command-line args.
+ kwargs['testRunner'] = xml_test_runner_class
+ if kwargs.get('testRunner') is None:
+ kwargs['testRunner'] = xml_test_runner_class
+ # Use an in-memory buffer (not backed by the actual file) to store the XML
+ # report, because some tools modify the file (e.g., create a placeholder
+ # with partial information, in case the test process crashes).
+ xml_buffer = io.StringIO()
+ kwargs['testRunner'].set_default_xml_stream(xml_buffer) # pytype: disable=attribute-error
+
+ # If we've used a seed to randomize test case ordering, we want to record it
+ # as a top-level attribute in the `testsuites` section of the XML output.
+ randomize_ordering_seed = getattr(
+ kwargs['testLoader'], '_randomize_ordering_seed', None)
+ setter = getattr(kwargs['testRunner'], 'set_testsuites_property', None)
+ if randomize_ordering_seed and setter:
+ setter('test_randomize_ordering_seed', randomize_ordering_seed)
+ elif kwargs.get('testRunner') is None:
+ kwargs['testRunner'] = _pretty_print_reporter.TextTestRunner
+
+ if FLAGS.pdb_post_mortem:
+ runner = kwargs['testRunner']
+ # testRunner can be a class or an instance, which must be tested for
+ # differently.
+ # Overriding testRunner isn't uncommon, so only enable the debugging
+ # integration if the runner claims it does; we don't want to accidentally
+ # clobber something on the runner.
+ if ((isinstance(runner, type) and
+ issubclass(runner, _pretty_print_reporter.TextTestRunner)) or
+ isinstance(runner, _pretty_print_reporter.TextTestRunner)):
+ runner.run_for_debugging = True
+
+ # Make sure tmpdir exists.
+ if not os.path.isdir(TEST_TMPDIR.value):
+ try:
+ os.makedirs(TEST_TMPDIR.value)
+ except OSError as e:
+ # Concurrent test might have created the directory.
+ if e.errno != errno.EEXIST:
+ raise
+
+ # Let unittest.TestProgram.__init__ do its own argv parsing, e.g. for '-v',
+ # on argv, which is sys.argv without the command-line flags.
+ kwargs['argv'] = argv
+
+ # Request unittest.TestProgram to not exit. The exit will be handled by
+ # `absltest.run_tests`.
+ kwargs['exit'] = False
+
+ try:
+ test_program = unittest.TestProgram(*args, **kwargs)
+ return test_program.result, fail_when_no_tests_ran
+ finally:
+ if xml_buffer:
+ try:
+ with _open(xml_output_file, 'w') as f:
+ f.write(xml_buffer.getvalue())
+ finally:
+ xml_buffer.close()
+
+
+def run_tests(
+ argv: abc.MutableSequence[str],
+ args: abc.Sequence[Any],
+ kwargs: abc.MutableMapping[str, Any],
+) -> None:
+ """Executes a set of Python unit tests.
+
+ Most users should call absltest.main() instead of run_tests.
+
+ Please note that run_tests should be called from app.run.
+ Calling absltest.main() would ensure that.
+
+ Please note that run_tests is allowed to make changes to kwargs.
+
+ Args:
+ argv: sys.argv with the command-line flags removed from the front, i.e. the
+ argv with which :func:`app.run()` has called
+ ``__main__.main``. It is passed to
+ ``unittest.TestProgram.__init__(argv=)``, which does its own flag parsing.
+ It is ignored if kwargs contains an argv entry.
+ args: Positional arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ kwargs: Keyword arguments passed through to
+ ``unittest.TestProgram.__init__``.
+ """
+ result, fail_when_no_tests_ran = _run_and_get_tests_result(
+ argv, args, kwargs, xml_reporter.TextAndXMLTestRunner
+ )
+ if fail_when_no_tests_ran and result.testsRun == 0 and not result.skipped:
+ # Python 3.12 unittest exits with 5 when no tests ran. The exit code 5 comes
+ # from pytest which does the same thing.
+ sys.exit(5)
+ sys.exit(not result.wasSuccessful())
+
+
+def _rmtree_ignore_errors(path: str) -> None:
+ if os.path.isfile(path):
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+ else:
+ shutil.rmtree(path, ignore_errors=True)
+
+
+def _get_first_part(path: str) -> str:
+ parts = path.split(os.sep, 1)
+ return parts[0]
diff --git a/venv/Lib/site-packages/absl/testing/flagsaver.py b/venv/Lib/site-packages/absl/testing/flagsaver.py
new file mode 100644
index 0000000000000000000000000000000000000000..508491808daf25b5242d923fa3f88978b30c3730
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/flagsaver.py
@@ -0,0 +1,403 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Decorator and context manager for saving and restoring flag values.
+
+There are many ways to save and restore. Always use the most convenient method
+for a given use case.
+
+Here are examples of each method. They all call ``do_stuff()`` while
+``FLAGS.someflag`` is temporarily set to ``'foo'``::
+
+ from absl.testing import flagsaver
+
+ # Use a decorator which can optionally override flags via arguments.
+ @flagsaver.flagsaver(someflag='foo')
+ def some_func():
+ do_stuff()
+
+ # Use a decorator which can optionally override flags with flagholders.
+ @flagsaver.flagsaver((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, 23))
+ def some_func():
+ do_stuff()
+
+ # Use a decorator which does not override flags itself.
+ @flagsaver.flagsaver
+ def some_func():
+ FLAGS.someflag = 'foo'
+ do_stuff()
+
+ # Use a context manager which can optionally override flags via arguments.
+ with flagsaver.flagsaver(someflag='foo'):
+ do_stuff()
+
+ # Save and restore the flag values yourself.
+ saved_flag_values = flagsaver.save_flag_values()
+ try:
+ FLAGS.someflag = 'foo'
+ do_stuff()
+ finally:
+ flagsaver.restore_flag_values(saved_flag_values)
+
+ # Use the parsing version to emulate users providing the flags.
+ # Note that all flags must be provided as strings (unparsed).
+ @flagsaver.as_parsed(some_int_flag='123')
+ def some_func():
+ # Because the flag was parsed it is considered "present".
+ assert FLAGS.some_int_flag.present
+ do_stuff()
+
+ # flagsaver.as_parsed() can also be used as a context manager just like
+ # flagsaver.flagsaver()
+ with flagsaver.as_parsed(some_int_flag='123'):
+ do_stuff()
+
+ # The flagsaver.as_parsed() interface also supports FlagHolder objects.
+ @flagsaver.as_parsed((module.FOO_FLAG, 'foo'), (other_mod.BAR_FLAG, '23'))
+ def some_func():
+ do_stuff()
+
+ # Using as_parsed with a multi_X flag requires a sequence of strings.
+ @flagsaver.as_parsed(some_multi_int_flag=['123', '456'])
+ def some_func():
+ assert FLAGS.some_multi_int_flag.present
+ do_stuff()
+
+ # If a flag name includes non-identifier characters it can be specified like
+ # so:
+ @flagsaver.as_parsed(**{'i-like-dashes': 'true'})
+ def some_func():
+ do_stuff()
+
+We save and restore a shallow copy of each Flag object's ``__dict__`` attribute.
+This preserves all attributes of the flag, such as whether or not it was
+overridden from its default value.
+
+WARNING: Currently a flag that is saved and then deleted cannot be restored. An
+exception will be raised. However if you *add* a flag after saving flag values,
+and then restore flag values, the added flag will be deleted with no errors.
+"""
+
+from collections.abc import Callable, Mapping, Sequence
+import functools
+import inspect
+from typing import Any, TypeVar, overload
+
+from absl import flags
+
+FLAGS = flags.FLAGS
+
+
+# The type of pre/post wrapped functions.
+_CallableT = TypeVar('_CallableT', bound=Callable)
+
+
+@overload
+def flagsaver(func: _CallableT) -> _CallableT:
+ ...
+
+
+@overload
+def flagsaver(
+ *args: tuple[flags.FlagHolder, Any], **kwargs: Any
+) -> '_FlagOverrider':
+ ...
+
+
+def flagsaver(*args, **kwargs):
+ """The main flagsaver interface. See module doc for usage."""
+ return _construct_overrider(_FlagOverrider, *args, **kwargs) # type: ignore[bad-return-type]
+
+
+@overload
+def as_parsed(
+ *args: tuple[flags.FlagHolder, str | Sequence[str]],
+ **kwargs: str | Sequence[str],
+) -> '_ParsingFlagOverrider':
+ ...
+
+
+@overload
+def as_parsed(func: _CallableT) -> _CallableT:
+ ...
+
+
+def as_parsed(*args, **kwargs):
+ """Overrides flags by parsing strings, saves flag state similar to flagsaver.
+
+ This function can be used as either a decorator or context manager similar to
+ flagsaver.flagsaver(). However, where flagsaver.flagsaver() directly sets the
+ flags to new values, this function will parse the provided arguments as if
+ they were provided on the command line. Among other things, this will cause
+ `FLAGS['flag_name'].present == True`.
+
+ A note on unparsed input: For many flag types, the unparsed version will be
+ a single string. However for multi_x (multi_string, multi_integer, multi_enum)
+ the unparsed version will be a Sequence of strings.
+
+ Args:
+ *args: Tuples of FlagHolders and their unparsed value.
+ **kwargs: The keyword args are flag names, and the values are unparsed
+ values.
+
+ Returns:
+ _ParsingFlagOverrider that serves as a context manager or decorator. Will
+ save previous flag state and parse new flags, then on cleanup it will
+ restore the previous flag state.
+ """
+ return _construct_overrider(_ParsingFlagOverrider, *args, **kwargs)
+
+
+# NOTE: the order of these overload declarations matters. The type checker will
+# pick the first match which could be incorrect.
+@overload
+def _construct_overrider(
+ flag_overrider_cls: type['_ParsingFlagOverrider'],
+ *args: tuple[flags.FlagHolder, str | Sequence[str]],
+ **kwargs: str | Sequence[str],
+) -> '_ParsingFlagOverrider':
+ ...
+
+
+@overload
+def _construct_overrider(
+ flag_overrider_cls: type['_FlagOverrider'], func: _CallableT
+) -> _CallableT:
+ ...
+
+
+@overload
+def _construct_overrider(
+ flag_overrider_cls: type['_FlagOverrider'],
+ *args: tuple[flags.FlagHolder, Any],
+ **kwargs: Any,
+) -> '_FlagOverrider':
+ ...
+
+
+def _construct_overrider(flag_overrider_cls, *args, **kwargs):
+ """Handles the args/kwargs returning an instance of flag_overrider_cls.
+
+ If flag_overrider_cls is _FlagOverrider then values should be native python
+ types matching the python types. Otherwise if flag_overrider_cls is
+ _ParsingFlagOverrider the values should be strings or sequences of strings.
+
+ Args:
+ flag_overrider_cls: The class that will do the overriding.
+ *args: Tuples of FlagHolder and the new flag value.
+ **kwargs: Keword args mapping flag name to new flag value.
+
+ Returns:
+ A _FlagOverrider to be used as a decorator or context manager.
+ """
+ if not args:
+ return flag_overrider_cls(**kwargs)
+ # args can be [func] if used as `@flagsaver` instead of `@flagsaver(...)`
+ if len(args) == 1 and callable(args[0]):
+ if kwargs:
+ raise ValueError(
+ "It's invalid to specify both positional and keyword parameters.")
+ func = args[0]
+ if inspect.isclass(func):
+ raise TypeError('@flagsaver.flagsaver cannot be applied to a class.')
+ return _wrap(flag_overrider_cls, func, {})
+ # args can be a list of (FlagHolder, value) pairs.
+ # In which case they augment any specified kwargs.
+ for arg in args:
+ if not isinstance(arg, tuple) or len(arg) != 2:
+ raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,))
+ holder, value = arg
+ if not isinstance(holder, flags.FlagHolder):
+ raise ValueError('Expected (FlagHolder, value) pair, found %r' % (arg,))
+ if holder.name in kwargs:
+ raise ValueError('Cannot set --%s multiple times' % holder.name)
+ kwargs[holder.name] = value
+ return flag_overrider_cls(**kwargs)
+
+
+def save_flag_values(
+ flag_values: flags.FlagValues = FLAGS,
+) -> dict[str, dict[str, Any]]:
+ """Returns copy of flag values as a dict.
+
+ Args:
+ flag_values: FlagValues, the FlagValues instance with which the flag will be
+ saved. This should almost never need to be overridden.
+
+ Returns:
+ Dictionary mapping keys to values. Keys are flag names, values are
+ corresponding ``__dict__`` members. E.g. ``{'key': value_dict, ...}``.
+ """
+ return {name: _copy_flag_dict(flag_values[name]) for name in flag_values}
+
+
+def restore_flag_values(
+ saved_flag_values: Mapping[str, dict[str, Any]],
+ flag_values: flags.FlagValues = FLAGS,
+) -> None:
+ """Restores flag values based on the dictionary of flag values.
+
+ Args:
+ saved_flag_values: {'flag_name': value_dict, ...}
+ flag_values: FlagValues, the FlagValues instance from which the flag will be
+ restored. This should almost never need to be overridden.
+ """
+ new_flag_names = list(flag_values)
+ for name in new_flag_names:
+ saved = saved_flag_values.get(name)
+ if saved is None:
+ # If __dict__ was not saved delete "new" flag.
+ delattr(flag_values, name)
+ else:
+ if flag_values[name].value != saved['_value']:
+ flag_values[name].value = saved['_value'] # Ensure C++ value is set.
+ flag_values[name].__dict__ = saved
+
+
+@overload
+def _wrap(
+ flag_overrider_cls: type['_FlagOverrider'],
+ func: _CallableT,
+ overrides: Mapping[str, Any],
+) -> _CallableT:
+ ...
+
+
+@overload
+def _wrap(
+ flag_overrider_cls: type['_ParsingFlagOverrider'],
+ func: _CallableT,
+ overrides: Mapping[str, str | Sequence[str]],
+) -> _CallableT:
+ ...
+
+
+def _wrap(flag_overrider_cls, func, overrides):
+ """Creates a wrapper function that saves/restores flag values.
+
+ Args:
+ flag_overrider_cls: The class that will be used as a context manager.
+ func: This will be called between saving flags and restoring flags.
+ overrides: Flag names mapped to their values. These flags will be set after
+ saving the original flag state. The type of the values depends on if
+ _FlagOverrider or _ParsingFlagOverrider was specified.
+
+ Returns:
+ A wrapped version of func.
+ """
+
+ @functools.wraps(func)
+ def _flagsaver_wrapper(*args, **kwargs):
+ """Wrapper function that saves and restores flags."""
+ with flag_overrider_cls(**overrides):
+ return func(*args, **kwargs)
+
+ return _flagsaver_wrapper
+
+
+class _FlagOverrider:
+ """Overrides flags for the duration of the decorated function call.
+
+ It also restores all original values of flags after decorated method
+ completes.
+ """
+
+ def __init__(self, **overrides: Any):
+ self._overrides = overrides
+ self._saved_flag_values = None
+
+ def __call__(self, func: _CallableT) -> _CallableT:
+ if inspect.isclass(func):
+ raise TypeError('flagsaver cannot be applied to a class.')
+ return _wrap(self.__class__, func, self._overrides)
+
+ def __enter__(self):
+ self._saved_flag_values = save_flag_values(FLAGS)
+ try:
+ FLAGS._set_attributes(**self._overrides)
+ except:
+ # It may fail because of flag validators.
+ restore_flag_values(self._saved_flag_values, FLAGS)
+ raise
+
+ def __exit__(self, exc_type, exc_value, traceback):
+ restore_flag_values(self._saved_flag_values, FLAGS)
+
+
+class _ParsingFlagOverrider(_FlagOverrider):
+ """Context manager for overriding flags.
+
+ Simulates command line parsing.
+
+ This is simlar to _FlagOverrider except that all **overrides should be
+ strings or sequences of strings, and when context is entered this class calls
+ .parse(value)
+
+ This results in the flags having .present set properly.
+ """
+
+ def __init__(self, **overrides: str | Sequence[str]):
+ for flag_name, new_value in overrides.items():
+ if isinstance(new_value, str):
+ continue
+ if isinstance(new_value, Sequence) and all(
+ isinstance(single_value, str) for single_value in new_value
+ ):
+ continue
+ raise TypeError(
+ f'flagsaver.as_parsed() cannot parse {flag_name}. Expected a single '
+ f'string or sequence of strings but {type(new_value)} was provided.')
+ super().__init__(**overrides)
+
+ def __enter__(self):
+ self._saved_flag_values = save_flag_values(FLAGS)
+ try:
+ for flag_name, unparsed_value in self._overrides.items():
+ # LINT.IfChange(flag_override_parsing)
+ FLAGS[flag_name].parse(unparsed_value)
+ FLAGS[flag_name].using_default_value = False
+ # LINT.ThenChange()
+
+ # Perform the validation on all modified flags. This is something that
+ # FLAGS._set_attributes() does for you in _FlagOverrider.
+ for flag_name in self._overrides:
+ FLAGS._assert_validators(FLAGS[flag_name].validators)
+
+ except KeyError as e:
+ # If a flag doesn't exist, an UnrecognizedFlagError is more specific.
+ restore_flag_values(self._saved_flag_values, FLAGS)
+ raise flags.UnrecognizedFlagError('Unknown command line flag.') from e
+
+ except:
+ # It may fail because of flag validators or general parsing issues.
+ restore_flag_values(self._saved_flag_values, FLAGS)
+ raise
+
+
+def _copy_flag_dict(flag: flags.Flag) -> dict[str, Any]:
+ """Returns a copy of the flag object's ``__dict__``.
+
+ It's mostly a shallow copy of the ``__dict__``, except it also does a shallow
+ copy of the validator list.
+
+ Args:
+ flag: flags.Flag, the flag to copy.
+
+ Returns:
+ A copy of the flag object's ``__dict__``.
+ """
+ copy = flag.__dict__.copy()
+ copy['_value'] = flag.value # Ensure correct restore for C++ flags.
+ copy['validators'] = list(flag.validators)
+ return copy
diff --git a/venv/Lib/site-packages/absl/testing/parameterized.py b/venv/Lib/site-packages/absl/testing/parameterized.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ee91258a702aa6db489edc0d276492f3afd5386
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/parameterized.py
@@ -0,0 +1,726 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Adds support for parameterized tests to Python's unittest TestCase class.
+
+A parameterized test is a method in a test case that is invoked with different
+argument tuples.
+
+A simple example::
+
+ class AdditionExample(parameterized.TestCase):
+ @parameterized.parameters(
+ (1, 2, 3),
+ (4, 5, 9),
+ (1, 1, 3))
+ def testAddition(self, op1, op2, result):
+ self.assertEqual(result, op1 + op2)
+
+Each invocation is a separate test case and properly isolated just
+like a normal test method, with its own setUp/tearDown cycle. In the
+example above, there are three separate testcases, one of which will
+fail due to an assertion error (1 + 1 != 3).
+
+Parameters for individual test cases can be tuples (with positional parameters)
+or dictionaries (with named parameters)::
+
+ class AdditionExample(parameterized.TestCase):
+ @parameterized.parameters(
+ {'op1': 1, 'op2': 2, 'result': 3},
+ {'op1': 4, 'op2': 5, 'result': 9},
+ )
+ def testAddition(self, op1, op2, result):
+ self.assertEqual(result, op1 + op2)
+
+If a parameterized test fails, the error message will show the
+original test name and the parameters for that test.
+
+The id method of the test, used internally by the unittest framework, is also
+modified to show the arguments (but note that the name reported by `id()`
+doesn't match the actual test name, see below). To make sure that test names
+stay the same across several invocations, object representations like::
+
+ >>> class Foo(object):
+ ... pass
+ >>> repr(Foo())
+ '<__main__.Foo object at 0x23d8610>'
+
+are turned into ``__main__.Foo``. When selecting a subset of test cases to run
+on the command-line, the test cases contain an index suffix for each argument
+in the order they were passed to :func:`parameters` (eg. testAddition0,
+testAddition1, etc.) This naming scheme is subject to change; for more reliable
+and stable names, especially in test logs, use :func:`named_parameters` instead.
+
+Tests using :func:`named_parameters` are similar to :func:`parameters`, except
+only tuples or dicts of args are supported. For tuples, the first parameter arg
+has to be a string (or an object that returns an apt name when converted via
+``str()``). For dicts, a value for the key ``testcase_name`` must be present and
+must be a string (or an object that returns an apt name when converted via
+``str()``)::
+
+ class NamedExample(parameterized.TestCase):
+ @parameterized.named_parameters(
+ ('Normal', 'aa', 'aaa', True),
+ ('EmptyPrefix', '', 'abc', True),
+ ('BothEmpty', '', '', True))
+ def testStartsWith(self, prefix, string, result):
+ self.assertEqual(result, string.startswith(prefix))
+
+ class NamedExample(parameterized.TestCase):
+ @parameterized.named_parameters(
+ {'testcase_name': 'Normal',
+ 'result': True, 'string': 'aaa', 'prefix': 'aa'},
+ {'testcase_name': 'EmptyPrefix',
+ 'result': True, 'string': 'abc', 'prefix': ''},
+ {'testcase_name': 'BothEmpty',
+ 'result': True, 'string': '', 'prefix': ''})
+ def testStartsWith(self, prefix, string, result):
+ self.assertEqual(result, string.startswith(prefix))
+
+Named tests also have the benefit that they can be run individually
+from the command line::
+
+ $ testmodule.py NamedExample.testStartsWithNormal
+ .
+ --------------------------------------------------------------------
+ Ran 1 test in 0.000s
+
+ OK
+
+Parameterized Classes
+=====================
+
+If invocation arguments are shared across test methods in a single
+TestCase class, instead of decorating all test methods
+individually, the class itself can be decorated::
+
+ @parameterized.parameters(
+ (1, 2, 3),
+ (4, 5, 9))
+ class ArithmeticTest(parameterized.TestCase):
+ def testAdd(self, arg1, arg2, result):
+ self.assertEqual(arg1 + arg2, result)
+
+ def testSubtract(self, arg1, arg2, result):
+ self.assertEqual(result - arg1, arg2)
+
+Inputs from Iterables
+=====================
+
+If parameters should be shared across several test cases, or are dynamically
+created from other sources, a single non-tuple iterable can be passed into
+the decorator. This iterable will be used to obtain the test cases::
+
+ class AdditionExample(parameterized.TestCase):
+ @parameterized.parameters(
+ c.op1, c.op2, c.result for c in testcases
+ )
+ def testAddition(self, op1, op2, result):
+ self.assertEqual(result, op1 + op2)
+
+
+Single-Argument Test Methods
+============================
+
+If a test method takes only one argument, the single arguments must not be
+wrapped into a tuple::
+
+ class NegativeNumberExample(parameterized.TestCase):
+ @parameterized.parameters(
+ -1, -3, -4, -5
+ )
+ def testIsNegative(self, arg):
+ self.assertTrue(IsNegative(arg))
+
+
+List/tuple as a Single Argument
+===============================
+
+If a test method takes a single argument of a list/tuple, it must be wrapped
+inside a tuple::
+
+ class ZeroSumExample(parameterized.TestCase):
+ @parameterized.parameters(
+ ([-1, 0, 1], ),
+ ([-2, 0, 2], ),
+ )
+ def testSumIsZero(self, arg):
+ self.assertEqual(0, sum(arg))
+
+
+Cartesian product of Parameter Values as Parameterized Test Cases
+=================================================================
+
+If required to test method over a cartesian product of parameters,
+`parameterized.product` may be used to facilitate generation of parameters
+test combinations::
+
+ class TestModuloExample(parameterized.TestCase):
+ @parameterized.product(
+ num=[0, 20, 80],
+ modulo=[2, 4],
+ expected=[0]
+ )
+ def testModuloResult(self, num, modulo, expected):
+ self.assertEqual(expected, num % modulo)
+
+This results in 6 test cases being created - one for each combination of the
+parameters. It is also possible to supply sequences of keyword argument dicts
+as elements of the cartesian product::
+
+ @parameterized.product(
+ (dict(num=5, modulo=3, expected=2),
+ dict(num=7, modulo=4, expected=3)),
+ dtype=(int, float)
+ )
+ def testModuloResult(self, num, modulo, expected, dtype):
+ self.assertEqual(expected, dtype(num) % modulo)
+
+This results in 4 test cases being created - for each of the two sets of test
+data (supplied as kwarg dicts) and for each of the two data types (supplied as
+a named parameter). Multiple keyword argument dicts may be supplied if required.
+
+Async Support
+=============
+
+If a test needs to call async functions, it can inherit from both
+parameterized.TestCase and another TestCase that supports async calls, such
+as [asynctest](https://github.com/Martiusweb/asynctest)::
+
+ import asynctest
+
+ class AsyncExample(parameterized.TestCase, asynctest.TestCase):
+ @parameterized.parameters(
+ ('a', 1),
+ ('b', 2),
+ )
+ async def testSomeAsyncFunction(self, arg, expected):
+ actual = await someAsyncFunction(arg)
+ self.assertEqual(actual, expected)
+"""
+
+from collections import abc
+import functools
+import inspect
+import itertools
+import re
+import types
+import unittest
+import warnings
+
+from absl.testing import absltest
+
+
+_ADDR_RE = re.compile(r'\<([a-zA-Z0-9_\-\.]+) object at 0x[a-fA-F0-9]+\>')
+_NAMED = object()
+_ARGUMENT_REPR = object()
+_NAMED_DICT_KEY = 'testcase_name'
+
+
+class NoTestsError(Exception):
+ """Raised when parameterized decorators do not generate any tests."""
+
+
+class DuplicateTestNameError(Exception):
+ """Raised when a parameterized test has the same test name multiple times."""
+
+ def __init__(self, test_class_name, new_test_name, original_test_name):
+ super().__init__(
+ 'Duplicate parameterized test name in {}: generated test name {!r} '
+ '(generated from {!r}) already exists. Consider using '
+ 'named_parameters() to give your tests unique names and/or renaming '
+ 'the conflicting test method.'.format(
+ test_class_name, new_test_name, original_test_name
+ )
+ )
+
+
+def _clean_repr(obj):
+ return _ADDR_RE.sub(r'<\1>', repr(obj))
+
+
+def _non_string_or_bytes_iterable(obj):
+ return (isinstance(obj, abc.Iterable) and not isinstance(obj, str) and
+ not isinstance(obj, bytes))
+
+
+def _format_parameter_list(testcase_params):
+ if isinstance(testcase_params, abc.Mapping):
+ return ', '.join('%s=%s' % (argname, _clean_repr(value))
+ for argname, value in testcase_params.items())
+ elif _non_string_or_bytes_iterable(testcase_params):
+ return ', '.join(map(_clean_repr, testcase_params))
+ else:
+ return _format_parameter_list((testcase_params,))
+
+
+def _async_wrapped(func):
+ @functools.wraps(func)
+ async def wrapper(*args, **kwargs):
+ return await func(*args, **kwargs)
+ return wrapper
+
+
+class _ParameterizedTestIter:
+ """Callable and iterable class for producing new test cases."""
+
+ def __init__(self, test_method, testcases, naming_type, original_name=None):
+ """Returns concrete test functions for a test and a list of parameters.
+
+ The naming_type is used to determine the name of the concrete
+ functions as reported by the unittest framework. If naming_type is
+ _FIRST_ARG, the testcases must be tuples, and the first element must
+ have a string representation that is a valid Python identifier.
+
+ Args:
+ test_method: The decorated test method.
+ testcases: (list of tuple/dict) A list of parameter tuples/dicts for
+ individual test invocations.
+ naming_type: The test naming type, either _NAMED or _ARGUMENT_REPR.
+ original_name: The original test method name. When decorated on a test
+ method, None is passed to __init__ and test_method.__name__ is used.
+ Note test_method.__name__ might be different than the original defined
+ test method because of the use of other decorators. A more accurate
+ value is set by TestGeneratorMetaclass.__new__ later.
+ """
+ self._test_method = test_method
+ self.testcases = testcases
+ self._naming_type = naming_type
+ if original_name is None:
+ original_name = test_method.__name__
+ self._original_name = original_name
+ self.__name__ = _ParameterizedTestIter.__name__
+
+ def __call__(self, *args, **kwargs):
+ raise RuntimeError('You appear to be running a parameterized test case '
+ 'without having inherited from parameterized.'
+ 'TestCase. This is bad because none of '
+ 'your test cases are actually being run. You may also '
+ 'be using another decorator before the parameterized '
+ 'one, in which case you should reverse the order.')
+
+ def __iter__(self):
+ test_method = self._test_method
+ naming_type = self._naming_type
+
+ def make_bound_param_test(testcase_params):
+ @functools.wraps(test_method)
+ def bound_param_test(self):
+ if isinstance(testcase_params, abc.Mapping):
+ return test_method(self, **testcase_params)
+ elif _non_string_or_bytes_iterable(testcase_params):
+ return test_method(self, *testcase_params)
+ else:
+ return test_method(self, testcase_params)
+
+ if naming_type is _NAMED:
+ # Signal the metaclass that the name of the test function is unique
+ # and descriptive.
+ bound_param_test.__x_use_name__ = True
+
+ testcase_name = None
+ if isinstance(testcase_params, abc.Mapping):
+ if _NAMED_DICT_KEY not in testcase_params:
+ raise RuntimeError(
+ 'Dict for named tests must contain key "%s"' % _NAMED_DICT_KEY)
+ # Create a new dict to avoid modifying the supplied testcase_params.
+ testcase_name = testcase_params[_NAMED_DICT_KEY]
+ testcase_params = {
+ k: v for k, v in testcase_params.items() if k != _NAMED_DICT_KEY
+ }
+ elif _non_string_or_bytes_iterable(testcase_params):
+ if not isinstance(testcase_params[0], str):
+ raise RuntimeError(
+ 'The first element of named test parameters is the test name '
+ 'suffix and must be a string')
+ testcase_name = testcase_params[0]
+ testcase_params = testcase_params[1:]
+ else:
+ raise RuntimeError(
+ 'Named tests must be passed a dict or non-string iterable.')
+
+ test_method_name = self._original_name
+ # Support PEP-8 underscore style for test naming if used.
+ if (test_method_name.startswith('test_')
+ and testcase_name
+ and not testcase_name.startswith('_')):
+ test_method_name += '_'
+
+ bound_param_test.__name__ = test_method_name + str(testcase_name)
+ elif naming_type is _ARGUMENT_REPR:
+ # If it's a generator, convert it to a tuple and treat them as
+ # parameters.
+ if isinstance(testcase_params, types.GeneratorType):
+ testcase_params = tuple(testcase_params)
+ # The metaclass creates a unique, but non-descriptive method name for
+ # _ARGUMENT_REPR tests using an indexed suffix.
+ # To keep test names descriptive, only the original method name is used.
+ # To make sure test names are unique, we add a unique descriptive suffix
+ # __x_params_repr__ for every test.
+ params_repr = '(%s)' % (_format_parameter_list(testcase_params),)
+ bound_param_test.__x_params_repr__ = params_repr
+ else:
+ raise RuntimeError('%s is not a valid naming type.' % (naming_type,))
+
+ bound_param_test.__doc__ = '%s(%s)' % (
+ bound_param_test.__name__, _format_parameter_list(testcase_params))
+ if test_method.__doc__:
+ bound_param_test.__doc__ += '\n%s' % (test_method.__doc__,)
+ if inspect.iscoroutinefunction(test_method):
+ return _async_wrapped(bound_param_test)
+ return bound_param_test
+
+ return (make_bound_param_test(c) for c in self.testcases)
+
+
+def _modify_class(class_object, testcases, naming_type):
+ assert not getattr(class_object, '_test_params_reprs', None), (
+ 'Cannot add parameters to %s. Either it already has parameterized '
+ 'methods, or its super class is also a parameterized class.' % (
+ class_object,))
+ # NOTE: _test_params_repr is private to parameterized.TestCase and it's
+ # metaclass; do not use it outside of those classes.
+ class_object._test_params_reprs = test_params_reprs = {}
+ for name, obj in class_object.__dict__.copy().items():
+ if (name.startswith(unittest.TestLoader.testMethodPrefix)
+ and isinstance(obj, types.FunctionType)):
+ delattr(class_object, name)
+ methods = {}
+ _update_class_dict_for_param_test_case(
+ class_object.__name__, methods, test_params_reprs, name,
+ _ParameterizedTestIter(obj, testcases, naming_type, name))
+ for meth_name, meth in methods.items():
+ setattr(class_object, meth_name, meth)
+
+
+def _parameter_decorator(naming_type, testcases):
+ """Implementation of the parameterization decorators.
+
+ Args:
+ naming_type: The naming type.
+ testcases: Testcase parameters.
+
+ Raises:
+ NoTestsError: Raised when the decorator generates no tests.
+
+ Returns:
+ A function for modifying the decorated object.
+ """
+ def _apply(obj):
+ if isinstance(obj, type):
+ _modify_class(obj, testcases, naming_type)
+ return obj
+ else:
+ return _ParameterizedTestIter(obj, testcases, naming_type)
+
+ if (len(testcases) == 1 and
+ not isinstance(testcases[0], tuple) and
+ not isinstance(testcases[0], abc.Mapping)):
+ # Support using a single non-tuple parameter as a list of test cases.
+ # Note that the single non-tuple parameter can't be Mapping either, which
+ # means a single dict parameter case.
+ assert _non_string_or_bytes_iterable(testcases[0]), (
+ 'Single parameter argument must be a non-string non-Mapping iterable')
+ testcases = testcases[0]
+
+ if not isinstance(testcases, abc.Sequence):
+ testcases = list(testcases)
+ if not testcases:
+ raise NoTestsError(
+ 'parameterized test decorators did not generate any tests. '
+ 'Make sure you specify non-empty parameters, '
+ 'and do not reuse generators more than once.')
+
+ return _apply
+
+
+def parameters(*testcases):
+ """A decorator for creating parameterized tests.
+
+ See the module docstring for a usage example.
+
+ Args:
+ *testcases: Parameters for the decorated method, either a single
+ iterable, or a list of tuples/dicts/objects (for tests with only one
+ argument).
+
+ Raises:
+ NoTestsError: Raised when the decorator generates no tests.
+
+ Returns:
+ A test generator to be handled by TestGeneratorMetaclass.
+ """
+ return _parameter_decorator(_ARGUMENT_REPR, testcases)
+
+
+def named_parameters(*testcases):
+ """A decorator for creating parameterized tests.
+
+ See the module docstring for a usage example. For every parameter tuple
+ passed, the first element of the tuple should be a string and will be appended
+ to the name of the test method. Each parameter dict passed must have a value
+ for the key "testcase_name", the string representation of that value will be
+ appended to the name of the test method.
+
+ Args:
+ *testcases: Parameters for the decorated method, either a single iterable,
+ or a list of tuples or dicts.
+
+ Raises:
+ NoTestsError: Raised when the decorator generates no tests.
+
+ Returns:
+ A test generator to be handled by TestGeneratorMetaclass.
+ """
+ return _parameter_decorator(_NAMED, testcases)
+
+
+def product(*kwargs_seqs, **testgrid):
+ """A decorator for running tests over cartesian product of parameters values.
+
+ See the module docstring for a usage example. The test will be run for every
+ possible combination of the parameters.
+
+ Args:
+ *kwargs_seqs: Each positional parameter is a sequence of keyword arg dicts;
+ every test case generated will include exactly one kwargs dict from each
+ positional parameter; these will then be merged to form an overall list
+ of arguments for the test case.
+ **testgrid: A mapping of parameter names and their possible values. Possible
+ values should given as either a list or a tuple.
+
+ Raises:
+ NoTestsError: Raised when the decorator generates no tests.
+
+ Returns:
+ A test generator to be handled by TestGeneratorMetaclass.
+ """
+
+ for name, values in testgrid.items():
+ assert isinstance(values, (list, tuple)), (
+ 'Values of {} must be given as list or tuple, found {}'.format(
+ name, type(values)))
+
+ prior_arg_names = set()
+ for kwargs_seq in kwargs_seqs:
+ assert ((isinstance(kwargs_seq, (list, tuple))) and
+ all(isinstance(kwargs, dict) for kwargs in kwargs_seq)), (
+ 'Positional parameters must be a sequence of keyword arg'
+ 'dicts, found {}'
+ .format(kwargs_seq))
+ if kwargs_seq:
+ arg_names = set(kwargs_seq[0])
+ assert all(set(kwargs) == arg_names for kwargs in kwargs_seq), (
+ 'Keyword argument dicts within a single parameter must all have the '
+ 'same keys, found {}'.format(kwargs_seq))
+ assert not (arg_names & prior_arg_names), (
+ 'Keyword argument dict sequences must all have distinct argument '
+ 'names, found duplicate(s) {}'
+ .format(sorted(arg_names & prior_arg_names)))
+ prior_arg_names |= arg_names
+
+ assert not (prior_arg_names & set(testgrid)), (
+ 'Arguments supplied in kwargs dicts in positional parameters must not '
+ 'overlap with arguments supplied as named parameters; found duplicate '
+ 'argument(s) {}'.format(sorted(prior_arg_names & set(testgrid))))
+
+ # Convert testgrid into a sequence of sequences of kwargs dicts and combine
+ # with the positional parameters.
+ # So foo=[1,2], bar=[3,4] --> [[{foo: 1}, {foo: 2}], [{bar: 3, bar: 4}]]
+ testgrid = (tuple({k: v} for v in vs) for k, vs in testgrid.items())
+ testgrid = tuple(kwargs_seqs) + tuple(testgrid)
+
+ # Create all possible combinations of parameters as a cartesian product
+ # of parameter values.
+ testcases = [
+ dict(itertools.chain.from_iterable(case.items()
+ for case in cases))
+ for cases in itertools.product(*testgrid)
+ ]
+ return _parameter_decorator(_ARGUMENT_REPR, testcases)
+
+
+class TestGeneratorMetaclass(type):
+ """Metaclass for adding tests generated by parameterized decorators."""
+
+ def __new__(cls, class_name, bases, dct):
+ # NOTE: _test_params_repr is private to parameterized.TestCase and it's
+ # metaclass; do not use it outside of those classes.
+ test_params_reprs = dct.setdefault('_test_params_reprs', {})
+ for name, obj in dct.copy().items():
+ if (name.startswith(unittest.TestLoader.testMethodPrefix) and
+ _non_string_or_bytes_iterable(obj)):
+ # NOTE: `obj` might not be a _ParameterizedTestIter in two cases:
+ # 1. a class-level iterable named test* that isn't a test, such as
+ # a list of something. Such attributes get deleted from the class.
+ #
+ # 2. If a decorator is applied to the parameterized test, e.g.
+ # @morestuff
+ # @parameterized.parameters(...)
+ # def test_foo(...): ...
+ #
+ # This is OK so long as the underlying parameterized function state
+ # is forwarded (e.g. using functool.wraps() and **without**
+ # accessing explicitly accessing the internal attributes.
+ if isinstance(obj, _ParameterizedTestIter):
+ # Update the original test method name so it's more accurate.
+ # The mismatch might happen when another decorator is used inside
+ # the parameterized decrators, and the inner decorator doesn't
+ # preserve its __name__.
+ obj._original_name = name
+ iterator = iter(obj)
+ dct.pop(name)
+ _update_class_dict_for_param_test_case(
+ class_name, dct, test_params_reprs, name, iterator)
+ # If the base class is a subclass of parameterized.TestCase, inherit its
+ # _test_params_reprs too.
+ for base in bases:
+ # Check if the base has _test_params_reprs first, then check if it's a
+ # subclass of parameterized.TestCase. Otherwise when this is called for
+ # the parameterized.TestCase definition itself, this raises because
+ # itself is not defined yet. This works as long as absltest.TestCase does
+ # not define _test_params_reprs.
+ base_test_params_reprs = getattr(base, '_test_params_reprs', None)
+ if base_test_params_reprs and issubclass(base, TestCase):
+ for test_method, test_method_id in base_test_params_reprs.items():
+ # test_method may both exists in base and this class.
+ # This class's method overrides base class's.
+ # That's why it should only inherit it if it does not exist.
+ test_params_reprs.setdefault(test_method, test_method_id)
+
+ return type.__new__(cls, class_name, bases, dct)
+
+
+def _update_class_dict_for_param_test_case(
+ test_class_name, dct, test_params_reprs, name, iterator):
+ """Adds individual test cases to a dictionary.
+
+ Args:
+ test_class_name: The name of the class tests are added to.
+ dct: The target dictionary.
+ test_params_reprs: The dictionary for mapping names to test IDs.
+ name: The original name of the test case.
+ iterator: The iterator generating the individual test cases.
+
+ Raises:
+ DuplicateTestNameError: Raised when a test name occurs multiple times.
+ RuntimeError: If non-parameterized functions are generated.
+ """
+ for idx, func in enumerate(iterator):
+ assert callable(func), 'Test generators must yield callables, got %r' % (
+ func,)
+ if not (getattr(func, '__x_use_name__', None) or
+ getattr(func, '__x_params_repr__', None)):
+ raise RuntimeError(
+ '{}.{} generated a test function without using the parameterized '
+ 'decorators. Only tests generated using the decorators are '
+ 'supported.'.format(test_class_name, name))
+
+ if getattr(func, '__x_use_name__', False):
+ original_name = func.__name__
+ new_name = original_name
+ else:
+ original_name = name
+ new_name = '%s%d' % (original_name, idx)
+
+ if new_name in dct:
+ raise DuplicateTestNameError(test_class_name, new_name, original_name)
+
+ dct[new_name] = func
+ test_params_reprs[new_name] = getattr(func, '__x_params_repr__', '')
+
+
+class TestCase(absltest.TestCase, metaclass=TestGeneratorMetaclass):
+ """Base class for test cases using the parameters decorator."""
+
+ # visibility: private; do not call outside this class.
+ def _get_params_repr(self):
+ return self._test_params_reprs.get(self._testMethodName, '')
+
+ def __str__(self):
+ params_repr = self._get_params_repr()
+ if params_repr:
+ params_repr = ' ' + params_repr
+ return '{}{} ({})'.format(
+ self._testMethodName, params_repr,
+ unittest.util.strclass(self.__class__))
+
+ def id(self):
+ """Returns the descriptive ID of the test.
+
+ This is used internally by the unittesting framework to get a name
+ for the test to be used in reports.
+
+ Returns:
+ The test id.
+ """
+ base = super().id()
+ params_repr = self._get_params_repr()
+ if params_repr:
+ # We include the params in the id so that, when reported in the
+ # test.xml file, the value is more informative than just "test_foo0".
+ # Use a space to separate them so that it's copy/paste friendly and
+ # easy to identify the actual test id.
+ return f'{base} {params_repr}'
+ else:
+ return base
+
+
+# This function is kept CamelCase because it's used as a class's base class.
+def CoopTestCase(other_base_class) -> type: # pylint: disable=invalid-name, g-bare-generic
+ """Returns a new base class with a cooperative metaclass base.
+
+ This enables the TestCase to be used in combination
+ with other base classes that have custom metaclasses, such as
+ ``mox.MoxTestBase``.
+
+ Only works with metaclasses that do not override ``type.__new__``.
+
+ Example::
+
+ from absl.testing import parameterized
+
+ class ExampleTest(parameterized.CoopTestCase(OtherTestCase)):
+ ...
+
+ Args:
+ other_base_class: (class) A test case base class.
+
+ Returns:
+ A new class object.
+ """
+ # If the other base class has a metaclass of 'type' then trying to combine
+ # the metaclasses will result in an MRO error. So simply combine them and
+ # return.
+ if type(other_base_class) == type: # pylint: disable=unidiomatic-typecheck
+ warnings.warn(
+ 'CoopTestCase is only necessary when combining with a class that uses'
+ ' a metaclass. Use multiple inheritance like this instead: class'
+ f' ExampleTest(paramaterized.TestCase, {other_base_class.__name__}):',
+ stacklevel=2,
+ )
+
+ class CoopTestCaseBase(other_base_class, TestCase):
+ pass
+
+ return CoopTestCaseBase
+ else:
+
+ class CoopMetaclass(type(other_base_class), TestGeneratorMetaclass): # type: ignore # pylint: disable=unused-variable
+ pass
+
+ class CoopTestCaseBase(other_base_class, TestCase, metaclass=CoopMetaclass): # type: ignore
+ pass
+
+ return CoopTestCaseBase
diff --git a/venv/Lib/site-packages/absl/testing/xml_reporter.py b/venv/Lib/site-packages/absl/testing/xml_reporter.py
new file mode 100644
index 0000000000000000000000000000000000000000..25114e94b1cb6a2565375ae861bb7be4a0344bf7
--- /dev/null
+++ b/venv/Lib/site-packages/absl/testing/xml_reporter.py
@@ -0,0 +1,570 @@
+# Copyright 2017 The Abseil Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""A Python test reporter that generates test reports in JUnit XML format."""
+
+import datetime
+import re
+import sys
+import threading
+import time
+import traceback
+from typing import Any
+import unittest
+from xml.sax import saxutils
+from absl.testing import _pretty_print_reporter
+
+
+# See http://www.w3.org/TR/REC-xml/#NT-Char
+_bad_control_character_codes = set(range(0, 0x20)) - {0x9, 0xA, 0xD}
+
+
+_control_character_conversions = {
+ chr(i): f'\\x{i:02x}' for i in _bad_control_character_codes
+}
+
+
+_escape_xml_attr_conversions = {
+ '"': '"',
+ "'": ''',
+ '\n': '
',
+ '\t': ' ',
+ '\r': '
',
+ ' ': ' '}
+_escape_xml_attr_conversions.update(_control_character_conversions)
+
+
+# When class or module level function fails, unittest/suite.py adds a
+# _ErrorHolder instance instead of a real TestCase, and it has a description
+# like "setUpClass (__main__.MyTestCase)".
+_CLASS_OR_MODULE_LEVEL_TEST_DESC_REGEX = re.compile(r'^(\w+) \((\S+)\)$')
+
+
+# NOTE: while saxutils.quoteattr() theoretically does the same thing; it
+# seems to often end up being too smart for it's own good not escaping properly.
+# This function is much more reliable.
+def _escape_xml_attr(content):
+ """Escapes xml attributes."""
+ # Note: saxutils doesn't escape the quotes.
+ return saxutils.escape(content, _escape_xml_attr_conversions)
+
+
+def _escape_cdata(s):
+ """Escapes a string to be used as XML CDATA.
+
+ CDATA characters are treated strictly as character data, not as XML markup,
+ but there are still certain restrictions on them.
+
+ Args:
+ s: the string to be escaped.
+ Returns:
+ An escaped version of the input string.
+ """
+ for char, escaped in _control_character_conversions.items():
+ s = s.replace(char, escaped)
+ return s.replace(']]>', ']] >')
+
+
+def _iso8601_timestamp(timestamp):
+ """Produces an ISO8601 datetime.
+
+ Args:
+ timestamp: an Epoch based timestamp in seconds.
+
+ Returns:
+ A iso8601 format timestamp if the input is a valid timestamp, None otherwise
+ """
+ if timestamp is None or timestamp < 0:
+ return None
+ return datetime.datetime.fromtimestamp(
+ timestamp, tz=datetime.timezone.utc).isoformat()
+
+
+def _print_xml_element_header(element, attributes, stream, indentation=''):
+ """Prints an XML header of an arbitrary element.
+
+ Args:
+ element: element name (testsuites, testsuite, testcase)
+ attributes: 2-tuple list with (attributes, values) already escaped
+ stream: output stream to write test report XML to
+ indentation: indentation added to the element header
+ """
+ stream.write('%s<%s' % (indentation, element))
+ for attribute in attributes:
+ if (len(attribute) == 2 and attribute[0] is not None and
+ attribute[1] is not None):
+ stream.write(' %s="%s"' % (attribute[0], attribute[1]))
+ stream.write('>\n')
+
+# Copy time.time which ensures the real time is used internally.
+# This prevents bad interactions with tests that stub out time.
+_time_copy = time.time
+
+
+def _safe_str(obj: object) -> str:
+ """Returns a string representation of an object."""
+ try:
+ return str(obj)
+ except Exception: # pylint: disable=broad-except
+ return '' % (
+ type(obj).__module__,
+ type(obj).__name__,
+ )
+
+
+class _TestCaseResult:
+ """Private helper for _TextAndXMLTestResult that represents a test result.
+
+ Attributes:
+ test: A TestCase instance of an individual test method.
+ name: The name of the individual test method.
+ full_class_name: The full name of the test class.
+ run_time: The duration (in seconds) it took to run the test.
+ start_time: Epoch relative timestamp of when test started (in seconds)
+ errors: A list of error 4-tuples. Error tuple entries are
+ 1) a string identifier of either "failure" or "error"
+ 2) an exception_type
+ 3) an exception_message
+ 4) a string version of a sys.exc_info()-style tuple of values
+ ('error', err[0], err[1], self._exc_info_to_string(err))
+ If the length of errors is 0, then the test is either passed or
+ skipped.
+ skip_reason: A string explaining why the test was skipped.
+ """
+
+ def __init__(self, test):
+ self.run_time = -1
+ self.start_time = -1
+ self.skip_reason = None
+ self.errors = []
+ self.test = test
+
+ # Parse the test id to get its test name and full class path.
+ # Unfortunately there is no better way of knowning the test and class.
+ # Worse, unittest uses _ErrorHandler instances to represent class / module
+ # level failures.
+ test_desc = test.id() or str(test)
+ # Check if it's something like "setUpClass (__main__.TestCase)".
+ match = _CLASS_OR_MODULE_LEVEL_TEST_DESC_REGEX.match(test_desc)
+ if match:
+ name = match.group(1)
+ full_class_name = match.group(2)
+ else:
+ class_name = unittest.util.strclass(test.__class__)
+ if isinstance(test, unittest.case._SubTest):
+ # If the test case is a _SubTest, the real TestCase instance is
+ # available as _SubTest.test_case.
+ class_name = unittest.util.strclass(test.test_case.__class__)
+ if test_desc.startswith(class_name + '.'):
+ # In a typical unittest.TestCase scenario, test.id() returns with
+ # a class name formatted using unittest.util.strclass.
+ name = test_desc[len(class_name)+1:]
+ full_class_name = class_name
+ else:
+ # Otherwise make a best effort to guess the test name and full class
+ # path.
+ parts = test_desc.rsplit('.', 1)
+ name = parts[-1]
+ full_class_name = parts[0] if len(parts) == 2 else ''
+ self.name = _escape_xml_attr(name)
+ self.full_class_name = _escape_xml_attr(full_class_name)
+
+ def set_run_time(self, time_in_secs):
+ self.run_time = time_in_secs
+
+ def set_start_time(self, time_in_secs):
+ self.start_time = time_in_secs
+
+ def print_xml_summary(self, stream):
+ """Prints an XML Summary of a TestCase.
+
+ Status and result are populated as per JUnit XML test result reporter.
+ A test that has been skipped will always have a skip reason,
+ as every skip method in Python's unittest requires the reason arg to be
+ passed.
+
+ Args:
+ stream: output stream to write test report XML to
+ """
+
+ if self.skip_reason is None:
+ status = 'run'
+ result = 'completed'
+ else:
+ status = 'notrun'
+ result = 'suppressed'
+
+ test_case_attributes = [
+ ('name', '%s' % self.name),
+ ('status', '%s' % status),
+ ('result', '%s' % result),
+ ('time', '%.3f' % self.run_time),
+ ('classname', self.full_class_name),
+ ('timestamp', _iso8601_timestamp(self.start_time)),
+ ]
+ _print_xml_element_header('testcase', test_case_attributes, stream, ' ')
+ self._print_testcase_details(stream)
+ stream.write(' \n')
+
+ def _print_testcase_details(self, stream):
+ for error in self.errors:
+ outcome, exception_type, message, error_msg = error # pylint: disable=unpacking-non-sequence
+ message = _escape_xml_attr(_safe_str(message))
+ exception_type = _escape_xml_attr(str(exception_type))
+ error_msg = _escape_cdata(error_msg)
+ stream.write(' <%s message="%s" type="%s">%s>\n'
+ % (outcome, message, exception_type, error_msg, outcome))
+
+
+class _TestSuiteResult:
+ """Private helper for _TextAndXMLTestResult."""
+
+ def __init__(self):
+ self.suites = {}
+ self.failure_counts = {}
+ self.error_counts = {}
+ self.overall_start_time = -1
+ self.overall_end_time = -1
+ self._testsuites_properties = {}
+
+ def add_test_case_result(self, test_case_result):
+ suite_name = type(test_case_result.test).__name__
+ if suite_name == '_ErrorHolder':
+ # _ErrorHolder is a special case created by unittest for class / module
+ # level functions.
+ suite_name = test_case_result.full_class_name.rsplit('.')[-1]
+ if isinstance(test_case_result.test, unittest.case._SubTest):
+ # If the test case is a _SubTest, the real TestCase instance is
+ # available as _SubTest.test_case.
+ suite_name = type(test_case_result.test.test_case).__name__
+
+ self._setup_test_suite(suite_name)
+ self.suites[suite_name].append(test_case_result)
+ for error in test_case_result.errors:
+ # Only count the first failure or error so that the sum is equal to the
+ # total number of *testcases* that have failures or errors.
+ if error[0] == 'failure':
+ self.failure_counts[suite_name] += 1
+ break
+ elif error[0] == 'error':
+ self.error_counts[suite_name] += 1
+ break
+
+ def print_xml_summary(self, stream):
+ overall_test_count = sum(len(x) for x in self.suites.values())
+ overall_failures = sum(self.failure_counts.values())
+ overall_errors = sum(self.error_counts.values())
+ overall_attributes = [
+ ('name', ''),
+ ('tests', '%d' % overall_test_count),
+ ('failures', '%d' % overall_failures),
+ ('errors', '%d' % overall_errors),
+ ('time', '%.3f' % (self.overall_end_time - self.overall_start_time)),
+ ('timestamp', _iso8601_timestamp(self.overall_start_time)),
+ ]
+ _print_xml_element_header('testsuites', overall_attributes, stream)
+ if self._testsuites_properties:
+ stream.write(' \n')
+ for name, value in sorted(self._testsuites_properties.items()):
+ stream.write(' \n' %
+ (_escape_xml_attr(name), _escape_xml_attr(str(value))))
+ stream.write(' \n')
+
+ for suite_name in self.suites:
+ suite = self.suites[suite_name]
+ suite_end_time = max(x.start_time + x.run_time for x in suite)
+ suite_start_time = min(x.start_time for x in suite)
+ failures = self.failure_counts[suite_name]
+ errors = self.error_counts[suite_name]
+ suite_attributes = [
+ ('name', '%s' % suite_name),
+ ('tests', '%d' % len(suite)),
+ ('failures', '%d' % failures),
+ ('errors', '%d' % errors),
+ ('time', '%.3f' % (suite_end_time - suite_start_time)),
+ ('timestamp', _iso8601_timestamp(suite_start_time)),
+ ]
+ _print_xml_element_header('testsuite', suite_attributes, stream)
+
+ # test_case_result entries are not guaranteed to be in any user-friendly
+ # order, especially when using subtests. So sort them.
+ for test_case_result in sorted(suite, key=lambda t: t.name):
+ test_case_result.print_xml_summary(stream)
+ stream.write('\n')
+ stream.write('\n')
+
+ def _setup_test_suite(self, suite_name):
+ """Adds a test suite to the set of suites tracked by this test run.
+
+ Args:
+ suite_name: string, The name of the test suite being initialized.
+ """
+ if suite_name in self.suites:
+ return
+ self.suites[suite_name] = []
+ self.failure_counts[suite_name] = 0
+ self.error_counts[suite_name] = 0
+
+ def set_end_time(self, timestamp_in_secs):
+ """Sets the start timestamp of this test suite.
+
+ Args:
+ timestamp_in_secs: timestamp in seconds since epoch
+ """
+ self.overall_end_time = timestamp_in_secs
+
+ def set_start_time(self, timestamp_in_secs):
+ """Sets the end timestamp of this test suite.
+
+ Args:
+ timestamp_in_secs: timestamp in seconds since epoch
+ """
+ self.overall_start_time = timestamp_in_secs
+
+
+class _TextAndXMLTestResult(_pretty_print_reporter.TextTestResult):
+ """Private TestResult class that produces both formatted text results and XML.
+
+ Used by TextAndXMLTestRunner.
+ """
+
+ _TEST_SUITE_RESULT_CLASS = _TestSuiteResult
+ _TEST_CASE_RESULT_CLASS = _TestCaseResult
+
+ def __init__(self, xml_stream, stream, descriptions, verbosity,
+ time_getter=_time_copy, testsuites_properties=None):
+ super().__init__(stream, descriptions, verbosity)
+ self.xml_stream = xml_stream
+ self.pending_test_case_results = {}
+ self.suite = self._TEST_SUITE_RESULT_CLASS()
+ if testsuites_properties:
+ self.suite._testsuites_properties = testsuites_properties
+ self.time_getter = time_getter
+
+ # This lock guards any mutations on pending_test_case_results.
+ self._pending_test_case_results_lock = threading.RLock()
+
+ def startTest(self, test):
+ self.start_time = self.time_getter()
+ super().startTest(test)
+
+ def stopTest(self, test):
+ # Grabbing the write lock to avoid conflicting with stopTestRun.
+ with self._pending_test_case_results_lock:
+ super().stopTest(test)
+ result = self.get_pending_test_case_result(test)
+ if not result:
+ test_name = test.id() or str(test)
+ sys.stderr.write('No pending test case: %s\n' % test_name)
+ return
+ if getattr(self, 'start_time', None) is None:
+ # startTest may not be called for skipped tests since Python 3.12.1.
+ self.start_time = self.time_getter()
+ test_id = id(test)
+ run_time = self.time_getter() - self.start_time
+ result.set_run_time(run_time)
+ result.set_start_time(self.start_time)
+ self.suite.add_test_case_result(result)
+ del self.pending_test_case_results[test_id]
+
+ def startTestRun(self):
+ self.suite.set_start_time(self.time_getter())
+ super().startTestRun()
+
+ def stopTestRun(self):
+ self.suite.set_end_time(self.time_getter())
+ # All pending_test_case_results will be added to the suite and removed from
+ # the pending_test_case_results dictionary. Grabbing the write lock to avoid
+ # results from being added during this process to avoid duplicating adds or
+ # accidentally erasing newly appended pending results.
+ with self._pending_test_case_results_lock:
+ # Errors in the test fixture (setUpModule, tearDownModule,
+ # setUpClass, tearDownClass) can leave a pending result which
+ # never gets added to the suite. The runner calls stopTestRun
+ # which gives us an opportunity to add these errors for
+ # reporting here.
+ for test_id in self.pending_test_case_results:
+ result = self.pending_test_case_results[test_id]
+ if getattr(self, 'start_time', None) is not None:
+ run_time = self.suite.overall_end_time - self.start_time
+ result.set_run_time(run_time)
+ result.set_start_time(self.start_time)
+ self.suite.add_test_case_result(result)
+ self.pending_test_case_results.clear()
+
+ def _exc_info_to_string(self, err, test=None):
+ """Converts a sys.exc_info()-style tuple of values into a string.
+
+ This method must be overridden because the method signature in
+ unittest.TestResult changed between Python 2.2 and 2.4.
+
+ Args:
+ err: A sys.exc_info() tuple of values for an error.
+ test: The test method.
+
+ Returns:
+ A formatted exception string.
+ """
+ if test:
+ return super()._exc_info_to_string(err, test)
+ return ''.join(traceback.format_exception(*err))
+
+ def add_pending_test_case_result(self, test, error_summary=None,
+ skip_reason=None):
+ """Adds result information to a test case result which may still be running.
+
+ If a result entry for the test already exists, add_pending_test_case_result
+ will add error summary tuples and/or overwrite skip_reason for the result.
+ If it does not yet exist, a result entry will be created.
+ Note that a test result is considered to have been run and passed
+ only if there are no errors or skip_reason.
+
+ Args:
+ test: A test method as defined by unittest
+ error_summary: A 4-tuple with the following entries:
+ 1) a string identifier of either "failure" or "error"
+ 2) an exception_type
+ 3) an exception_message
+ 4) a string version of a sys.exc_info()-style tuple of values
+ ('error', err[0], err[1], self._exc_info_to_string(err))
+ If the length of errors is 0, then the test is either passed or
+ skipped.
+ skip_reason: a string explaining why the test was skipped
+ """
+ with self._pending_test_case_results_lock:
+ test_id = id(test)
+ if test_id not in self.pending_test_case_results:
+ self.pending_test_case_results[test_id] = self._TEST_CASE_RESULT_CLASS(
+ test)
+ if error_summary:
+ self.pending_test_case_results[test_id].errors.append(error_summary)
+ if skip_reason:
+ self.pending_test_case_results[test_id].skip_reason = skip_reason
+
+ def delete_pending_test_case_result(self, test):
+ with self._pending_test_case_results_lock:
+ test_id = id(test)
+ del self.pending_test_case_results[test_id]
+
+ def get_pending_test_case_result(self, test):
+ test_id = id(test)
+ return self.pending_test_case_results.get(test_id, None)
+
+ def addSuccess(self, test):
+ super().addSuccess(test)
+ self.add_pending_test_case_result(test)
+
+ def addError(self, test, err):
+ super().addError(test, err)
+ error_summary = ('error', err[0], err[1],
+ self._exc_info_to_string(err, test=test))
+ self.add_pending_test_case_result(test, error_summary=error_summary)
+
+ def addFailure(self, test, err):
+ super().addFailure(test, err)
+ error_summary = ('failure', err[0], err[1],
+ self._exc_info_to_string(err, test=test))
+ self.add_pending_test_case_result(test, error_summary=error_summary)
+
+ def addSkip(self, test, reason):
+ super().addSkip(test, reason)
+ self.add_pending_test_case_result(test, skip_reason=reason)
+
+ def addExpectedFailure(self, test, err):
+ super().addExpectedFailure(test, err)
+ if callable(getattr(test, 'recordProperty', None)):
+ test.recordProperty('EXPECTED_FAILURE',
+ self._exc_info_to_string(err, test=test))
+ self.add_pending_test_case_result(test)
+
+ def addUnexpectedSuccess(self, test):
+ super().addUnexpectedSuccess(test)
+ test_name = test.id() or str(test)
+ error_summary = ('error', '', '',
+ 'Test case %s should have failed, but passed.'
+ % (test_name))
+ self.add_pending_test_case_result(test, error_summary=error_summary)
+
+ def addSubTest(self, test, subtest, err): # pylint: disable=invalid-name
+ super().addSubTest(test, subtest, err)
+ if err is not None:
+ if issubclass(err[0], test.failureException):
+ error_summary = ('failure', err[0], err[1],
+ self._exc_info_to_string(err, test=test))
+ else:
+ error_summary = ('error', err[0], err[1],
+ self._exc_info_to_string(err, test=test))
+ else:
+ error_summary = None
+ self.add_pending_test_case_result(subtest, error_summary=error_summary)
+
+ def printErrors(self):
+ super().printErrors()
+ self.xml_stream.write('\n')
+ self.suite.print_xml_summary(self.xml_stream)
+
+
+class TextAndXMLTestRunner(unittest.TextTestRunner):
+ """A test runner that produces both formatted text results and XML.
+
+ It prints out the names of tests as they are run, errors as they
+ occur, and a summary of the results at the end of the test run.
+ """
+
+ _TEST_RESULT_CLASS = _TextAndXMLTestResult
+
+ _xml_stream = None
+ _testsuites_properties: dict[Any, Any] = {}
+
+ def __init__(self, xml_stream=None, *args, **kwargs):
+ """Initialize a TextAndXMLTestRunner.
+
+ Args:
+ xml_stream: file-like or None; XML-formatted test results are output
+ via this object's write() method. If None (the default), the
+ new instance behaves as described in the set_default_xml_stream method
+ documentation below.
+ *args: passed unmodified to unittest.TextTestRunner.__init__.
+ **kwargs: passed unmodified to unittest.TextTestRunner.__init__.
+ """
+ super().__init__(*args, **kwargs)
+ if xml_stream is not None:
+ self._xml_stream = xml_stream
+ # else, do not set self._xml_stream to None -- this allows implicit fallback
+ # to the class attribute's value.
+
+ @classmethod
+ def set_default_xml_stream(cls, xml_stream):
+ """Sets the default XML stream for the class.
+
+ Args:
+ xml_stream: file-like or None; used for instances when xml_stream is None
+ or not passed to their constructors. If None is passed, instances
+ created with xml_stream=None will act as ordinary TextTestRunner
+ instances; this is the default state before any calls to this method
+ have been made.
+ """
+ cls._xml_stream = xml_stream
+
+ def _makeResult(self):
+ if self._xml_stream is None:
+ return super()._makeResult()
+ else:
+ return self._TEST_RESULT_CLASS(
+ self._xml_stream, self.stream, self.descriptions, self.verbosity,
+ testsuites_properties=self._testsuites_properties)
+
+ @classmethod
+ def set_testsuites_property(cls, key, value):
+ cls._testsuites_properties[key] = value
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/INSTALLER b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/INSTALLER
new file mode 100644
index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/METADATA b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/METADATA
new file mode 100644
index 0000000000000000000000000000000000000000..0cf2d721b265bf32a2ff733b34f8d2915c2c1aa6
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/METADATA
@@ -0,0 +1,100 @@
+Metadata-Version: 2.4
+Name: absl-py
+Version: 2.4.0
+Summary: Abseil Python Common Libraries, see https://github.com/abseil/abseil-py.
+Project-URL: Changelog, https://github.com/abseil/abseil-py/blob/main/CHANGELOG.md
+Project-URL: Documentation, https://abseil.io/docs/python/
+Project-URL: Issues, https://github.com/abseil/abseil-py/issues
+Project-URL: Source, https://github.com/abseil/abseil-py
+Author: The Abseil Authors
+License-Expression: Apache-2.0
+License-File: AUTHORS
+License-File: LICENSE
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Requires-Python: >=3.10
+Description-Content-Type: text/markdown
+
+[](https://pypi.org/project/absl-py)
+[](https://pypi.org/project/absl-py)
+[](https://github.com/abseil/abseil-py/blob/main/LICENSE)
+[](https://github.com/abseil/abseil-py/actions)
+[](https://pepy.tech/project/absl-py)
+[](https://pepy.tech/project/absl-py)
+
+# Abseil Python Common Libraries
+
+This repository is a collection of Python library code for building Python
+applications. The code is collected from Google's own Python code base, and has
+been extensively tested and used in production.
+
+## Features
+
+* Simple application startup
+* Distributed commandline flags system
+* Custom logging module with additional features
+* Testing utilities
+
+## Getting Started
+
+### Installation
+
+To install the package, simply run:
+
+```bash
+pip install absl-py
+```
+
+Or install from source:
+
+```bash
+pip install .
+```
+
+### Running Tests
+
+To run Abseil tests, you can clone the git repo and run
+[bazel](https://bazel.build/):
+
+```bash
+git clone https://github.com/abseil/abseil-py.git
+cd abseil-py
+bazel test absl/...
+```
+
+Please also validate the type annotations against the latest mypy:
+
+```bash
+pip install mypy
+mypy absl
+```
+
+### Example Code
+
+Please refer to
+[smoke_tests/sample_app.py](https://github.com/abseil/abseil-py/blob/main/smoke_tests/sample_app.py)
+as an example to get started.
+
+## Documentation
+
+See the [Abseil Python Developer Guide](https://abseil.io/docs/python/).
+
+## Future Releases
+
+The current repository includes an initial set of libraries for early adoption.
+More components and interoperability with Abseil C++ Common Libraries
+will come in future releases.
+
+## License
+
+The Abseil Python library is licensed under the terms of the Apache
+license. See [LICENSE](LICENSE) for more information.
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/RECORD b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/RECORD
new file mode 100644
index 0000000000000000000000000000000000000000..1408a4a15328ee1d8bf7d52037c57ca5596f1ef7
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/RECORD
@@ -0,0 +1,53 @@
+absl/__init__.py,sha256=tQJRn_5rbQ5gWoXdkGAV-fJSGFCOGH6fFBI1NxLx5ho,607
+absl/__pycache__/__init__.cpython-311.pyc,,
+absl/__pycache__/app.cpython-311.pyc,,
+absl/__pycache__/command_name.cpython-311.pyc,,
+absl/app.py,sha256=zC4tAKcK054F7kSrR7pzSCEDqNcJKLVb2AkvjIlmvl8,17065
+absl/app.pyi,sha256=YTcQS7o1DBFyFNgMpdykmvP3g4OljSwp3BuX9sSSLIM,1814
+absl/command_name.py,sha256=FgWUMHmlX0yQxEuMPXxFxn5ayWWZSLB0cq8Vx361TpU,2283
+absl/flags/__init__.py,sha256=n_uLeSK-15_1DKLb0wXivmt6A8xvADDC87D1yoJdqVU,7665
+absl/flags/__pycache__/__init__.cpython-311.pyc,,
+absl/flags/__pycache__/_argument_parser.cpython-311.pyc,,
+absl/flags/__pycache__/_defines.cpython-311.pyc,,
+absl/flags/__pycache__/_exceptions.cpython-311.pyc,,
+absl/flags/__pycache__/_flag.cpython-311.pyc,,
+absl/flags/__pycache__/_flagvalues.cpython-311.pyc,,
+absl/flags/__pycache__/_helpers.cpython-311.pyc,,
+absl/flags/__pycache__/_validators.cpython-311.pyc,,
+absl/flags/__pycache__/_validators_classes.cpython-311.pyc,,
+absl/flags/__pycache__/argparse_flags.cpython-311.pyc,,
+absl/flags/_argument_parser.py,sha256=XFIDxu1I8NjgBZhUdzSLUwDHhXFvOC-SbI8FsVmWZ_0,20531
+absl/flags/_defines.py,sha256=P3DLHYAaSgXSBlRgBN6QOS6oRcNUYNtzeapL5EQJ5LA,52499
+absl/flags/_exceptions.py,sha256=FZzlzhvkjqPImTxXqbS1pSPYKr_TvtOd5ellvoiVLDI,3619
+absl/flags/_flag.py,sha256=fOv_KC7t1e-BoabTP3iYHR1dUkTf2yhgfAWonjhByEQ,19891
+absl/flags/_flagvalues.py,sha256=QqCMTxHIJdwqYwlYhZ76nuXzsJQtF20RTzq-x0S_tPg,55319
+absl/flags/_helpers.py,sha256=0cWFV4XFLkcHWu-du6es8sCsWl26k1dZ6fqCPsWZ68k,13301
+absl/flags/_validators.py,sha256=VcsJtZzohliNxsI974NECYpeozD8rswHNHXggrQ4BLo,14140
+absl/flags/_validators_classes.py,sha256=PGUWzO7v3wPOHb9leIKKzry3q-pPeKCoMB_O7prLdnY,6093
+absl/flags/argparse_flags.py,sha256=usJudgMpy3P6Vvq7-LmJNa2Rj3ygHM3hwDTGd1mbAzc,14386
+absl/logging/__init__.py,sha256=3quWtll8Cb949CYs-VOC1AnxdBqClX8ab55VB28knVs,43883
+absl/logging/__init__.pyi,sha256=7yyq5rAJfkBFRaD8iqOTsLW-xhMAIDJ5ub4r3Has04U,5984
+absl/logging/__pycache__/__init__.cpython-311.pyc,,
+absl/logging/__pycache__/converter.cpython-311.pyc,,
+absl/logging/converter.py,sha256=6eBymfv9UNkog0BGat4HPWlxC_oSqvHcQ46jnSdtaMg,6323
+absl/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
+absl/testing/__init__.py,sha256=7cM57swk2T1Hc5wxmt-JpcaR6xfdPJyL_lyRqgODvuM,584
+absl/testing/__pycache__/__init__.cpython-311.pyc,,
+absl/testing/__pycache__/_bazelize_command.cpython-311.pyc,,
+absl/testing/__pycache__/_pretty_print_reporter.cpython-311.pyc,,
+absl/testing/__pycache__/absltest.cpython-311.pyc,,
+absl/testing/__pycache__/flagsaver.cpython-311.pyc,,
+absl/testing/__pycache__/parameterized.cpython-311.pyc,,
+absl/testing/__pycache__/xml_reporter.cpython-311.pyc,,
+absl/testing/_bazelize_command.py,sha256=qpioV02ln2sBBJ9kdlHgNpKk8_wxdz2hJGKbG6EWZMI,2287
+absl/testing/_pretty_print_reporter.py,sha256=PZh9NXSXBbXDi0FOk-BOmpse8LXa92Er16tgyBRogMs,3065
+absl/testing/absltest.py,sha256=CkUgJ54S4LJ1RwjNISv4pEW_N9kZBrNDpnzQ1YGPOlc,104586
+absl/testing/flagsaver.py,sha256=BeftLpNxJctYtd041tECgCSglQVFKSsQ4KBFT9D_m5k,13291
+absl/testing/parameterized.py,sha256=TpTlWTUXjikGeUDE45AgubvnmHsPbFNj-wUtAwT-e6E,27817
+absl/testing/xml_reporter.py,sha256=Q0keNHVOh3L9zXwNIzWCjXcWYlCM9ZJxdjMaYyHF3dw,21424
+absl_py-2.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
+absl_py-2.4.0.dist-info/METADATA,sha256=Eh8VboTinee1rj1-UDeBjw_KiicKBxJBi6H0zB1Sq0g,3283
+absl_py-2.4.0.dist-info/RECORD,,
+absl_py-2.4.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
+absl_py-2.4.0.dist-info/licenses/AUTHORS,sha256=YoLudsylaQg7W5mLn4FroQMuEnuNx8RpQrhkd_xvv6U,296
+absl_py-2.4.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/WHEEL b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/WHEEL
new file mode 100644
index 0000000000000000000000000000000000000000..ae8ec1bdaa94d726ceb907542d76cbd5d38cafcd
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/WHEEL
@@ -0,0 +1,4 @@
+Wheel-Version: 1.0
+Generator: hatchling 1.28.0
+Root-Is-Purelib: true
+Tag: py3-none-any
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/AUTHORS b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/AUTHORS
new file mode 100644
index 0000000000000000000000000000000000000000..23b11ada16bb8e69695cf52e5994784d98054e0d
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/AUTHORS
@@ -0,0 +1,7 @@
+# This is the list of Abseil authors for copyright purposes.
+#
+# This does not necessarily list everyone who has contributed code, since in
+# some cases, their employer may be the copyright holder. To see the full list
+# of contributors, see the revision history in source control.
+
+Google Inc.
diff --git a/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/LICENSE b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..d645695673349e3947e8e5ae42332d0ac3164cd7
--- /dev/null
+++ b/venv/Lib/site-packages/absl_py-2.4.0.dist-info/licenses/LICENSE
@@ -0,0 +1,202 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER b/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER
new file mode 100644
index 0000000000000000000000000000000000000000..a1b589e38a32041e49332e5e81c2d363dc418d68
--- /dev/null
+++ b/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER
@@ -0,0 +1 @@
+pip
diff --git a/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA b/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA
new file mode 100644
index 0000000000000000000000000000000000000000..c8a61b093c877752873e1eea4ae05af62b0171e3
--- /dev/null
+++ b/venv/Lib/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA
@@ -0,0 +1,123 @@
+Metadata-Version: 2.4
+Name: aiohappyeyeballs
+Version: 2.6.2
+Summary: Happy Eyeballs for asyncio
+License: PSF-2.0
+License-File: LICENSE
+Author: J. Nick Koston
+Author-email: nick@koston.org
+Requires-Python: >=3.10
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Intended Audience :: Developers
+Classifier: Natural Language :: English
+Classifier: Operating System :: OS Independent
+Classifier: Topic :: Software Development :: Libraries
+Classifier: Programming Language :: Python :: 3
+Classifier: Programming Language :: Python :: 3.10
+Classifier: Programming Language :: Python :: 3.11
+Classifier: Programming Language :: Python :: 3.12
+Classifier: Programming Language :: Python :: 3.13
+Classifier: License :: OSI Approved :: Python Software Foundation License
+Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues
+Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md
+Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io
+Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs
+Description-Content-Type: text/markdown
+
+# aiohappyeyeballs
+
+
"
+ f"\n{msg}\n\n"
+ )
+ ct = "text/html"
+ else:
+ if tb:
+ msg = tb
+ message = title + "\n\n" + msg
+
+ resp = Response(status=status, text=message, content_type=ct)
+ resp.force_close()
+
+ return resp
+
+ def _make_error_handler(
+ self, err_info: _ErrInfo
+ ) -> Callable[[BaseRequest], Awaitable[StreamResponse]]:
+ async def handler(request: BaseRequest) -> StreamResponse:
+ return self.handle_error(
+ request, err_info.status, err_info.exc, err_info.message
+ )
+
+ return handler
diff --git a/venv/Lib/site-packages/aiohttp/web_request.py b/venv/Lib/site-packages/aiohttp/web_request.py
new file mode 100644
index 0000000000000000000000000000000000000000..74dbd4846cc7354ca638444965e95e276cb4caa0
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_request.py
@@ -0,0 +1,948 @@
+import asyncio
+import datetime
+import io
+import re
+import string
+import tempfile
+import types
+import warnings
+from collections.abc import Iterator, Mapping, MutableMapping
+from re import Pattern
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar, cast, overload
+from urllib.parse import parse_qsl
+
+import attr
+from multidict import (
+ CIMultiDict,
+ CIMultiDictProxy,
+ MultiDict,
+ MultiDictProxy,
+ MultiMapping,
+)
+from yarl import URL
+
+from . import hdrs
+from ._cookie_helpers import parse_cookie_header
+from .abc import AbstractStreamWriter
+from .helpers import (
+ _SENTINEL,
+ DEBUG,
+ DEFAULT_CHUNK_SIZE,
+ ETAG_ANY,
+ LIST_QUOTED_ETAG_RE,
+ ChainMapProxy,
+ ETag,
+ HeadersMixin,
+ RequestKey,
+ parse_http_date,
+ reify,
+ sentinel,
+ set_exception,
+)
+from .http_parser import RawRequestMessage
+from .http_writer import HttpVersion
+from .multipart import BodyPartReader, MultipartReader
+from .streams import EmptyStreamReader, StreamReader
+from .typedefs import (
+ DEFAULT_JSON_DECODER,
+ JSONDecoder,
+ LooseHeaders,
+ RawHeaders,
+ StrOrURL,
+)
+from .web_exceptions import HTTPRequestEntityTooLarge, NotAppKeyWarning
+from .web_response import StreamResponse
+
+__all__ = ("BaseRequest", "FileField", "Request")
+
+
+if TYPE_CHECKING:
+ from .web_app import Application
+ from .web_protocol import RequestHandler
+ from .web_urldispatcher import UrlMappingMatchInfo
+
+
+_T = TypeVar("_T")
+
+
+@attr.s(auto_attribs=True, frozen=True, slots=True)
+class FileField:
+ name: str
+ filename: str
+ file: io.BufferedReader
+ content_type: str
+ headers: CIMultiDictProxy[str]
+
+
+_Post = str | bytes | bytearray | FileField
+_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-"
+# '-' at the end to prevent interpretation as range in a char class
+
+_TOKEN: Final[str] = rf"[{_TCHAR}]+"
+
+_QDTEXT: Final[str] = r"[{}]".format(
+ r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F)))
+)
+# qdtext includes 0x5C to escape 0x5D ('\]')
+# qdtext excludes obs-text (because obsoleted, and encoding not specified)
+
+_QUOTED_PAIR: Final[str] = r"\\[\t !-~]"
+
+_QUOTED_STRING: Final[str] = rf'"(?:{_QUOTED_PAIR}|{_QDTEXT})*"'
+
+# This does not have a ReDOS/performance concern as long as it used with re.match().
+_FORWARDED_PAIR: Final[str] = rf"({_TOKEN})=({_TOKEN}|{_QUOTED_STRING})(:\d{{1,4}})?"
+
+_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])")
+# same pattern as _QUOTED_PAIR but contains a capture group
+
+_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR)
+
+############################################################
+# HTTP Request
+############################################################
+
+
+class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin):
+ POST_METHODS = {
+ hdrs.METH_PATCH,
+ hdrs.METH_POST,
+ hdrs.METH_PUT,
+ hdrs.METH_TRACE,
+ hdrs.METH_DELETE,
+ }
+
+ ATTRS = HeadersMixin.ATTRS | frozenset(
+ [
+ "_message",
+ "_protocol",
+ "_payload_writer",
+ "_payload",
+ "_headers",
+ "_method",
+ "_version",
+ "_rel_url",
+ "_post",
+ "_read_bytes",
+ "_state",
+ "_cache",
+ "_task",
+ "_client_max_size",
+ "_loop",
+ "_transport_sslcontext",
+ "_transport_peername",
+ ]
+ )
+ _post: MultiDictProxy[_Post] | None = None
+ _read_bytes: bytes | None = None
+ _seen_str_keys: set[str] = set()
+
+ def __init__(
+ self,
+ message: RawRequestMessage,
+ payload: StreamReader,
+ protocol: "RequestHandler",
+ payload_writer: AbstractStreamWriter,
+ task: "asyncio.Task[None]",
+ loop: asyncio.AbstractEventLoop,
+ *,
+ client_max_size: int = 1024**2,
+ state: dict[RequestKey[Any] | str, Any] | None = None,
+ scheme: str | None = None,
+ host: str | None = None,
+ remote: str | None = None,
+ ) -> None:
+ self._message = message
+ self._protocol = protocol
+ self._payload_writer = payload_writer
+
+ self._payload = payload
+ self._headers: CIMultiDictProxy[str] = message.headers
+ self._method = message.method
+ self._version = message.version
+ self._cache: dict[str, Any] = {}
+ url = message.url
+ if url.absolute:
+ if scheme is not None:
+ url = url.with_scheme(scheme)
+ if host is not None:
+ url = url.with_host(host)
+ # absolute URL is given,
+ # override auto-calculating url, host, and scheme
+ # all other properties should be good
+ self._cache["url"] = url
+ self._cache["host"] = url.host
+ self._cache["scheme"] = url.scheme
+ self._rel_url = url.relative()
+ else:
+ self._rel_url = url
+ if scheme is not None:
+ self._cache["scheme"] = scheme
+ if host is not None:
+ self._cache["host"] = host
+
+ self._state = {} if state is None else state
+ self._task = task
+ self._client_max_size = client_max_size
+ self._loop = loop
+
+ self._transport_sslcontext = protocol.ssl_context
+ self._transport_peername = protocol.peername
+ self._transport_sockname = protocol.sockname
+
+ if remote is not None:
+ self._cache["remote"] = remote
+
+ def clone(
+ self,
+ *,
+ method: str | _SENTINEL = sentinel,
+ rel_url: StrOrURL | _SENTINEL = sentinel,
+ headers: LooseHeaders | _SENTINEL = sentinel,
+ scheme: str | _SENTINEL = sentinel,
+ host: str | _SENTINEL = sentinel,
+ remote: str | _SENTINEL = sentinel,
+ client_max_size: int | _SENTINEL = sentinel,
+ ) -> "BaseRequest":
+ """Clone itself with replacement some attributes.
+
+ Creates and returns a new instance of Request object. If no parameters
+ are given, an exact copy is returned. If a parameter is not passed, it
+ will reuse the one from the current request object.
+ """
+ if self._read_bytes:
+ raise RuntimeError("Cannot clone request after reading its content")
+
+ dct: dict[str, Any] = {}
+ if method is not sentinel:
+ dct["method"] = method
+ if rel_url is not sentinel:
+ new_url: URL = URL(rel_url)
+ dct["url"] = new_url
+ dct["path"] = str(new_url)
+ if headers is not sentinel:
+ # a copy semantic
+ dct["headers"] = CIMultiDictProxy(CIMultiDict(headers))
+ dct["raw_headers"] = tuple(
+ (k.encode("utf-8"), v.encode("utf-8"))
+ for k, v in dct["headers"].items()
+ )
+
+ message = self._message._replace(**dct)
+
+ kwargs = {}
+ if scheme is not sentinel:
+ kwargs["scheme"] = scheme
+ if host is not sentinel:
+ kwargs["host"] = host
+ if remote is not sentinel:
+ kwargs["remote"] = remote
+ if client_max_size is sentinel:
+ client_max_size = self._client_max_size
+
+ return self.__class__(
+ message,
+ self._payload,
+ self._protocol,
+ self._payload_writer,
+ self._task,
+ self._loop,
+ client_max_size=client_max_size,
+ state=self._state.copy(),
+ **kwargs,
+ )
+
+ @property
+ def task(self) -> "asyncio.Task[None]":
+ return self._task
+
+ @property
+ def protocol(self) -> "RequestHandler":
+ return self._protocol
+
+ @property
+ def transport(self) -> asyncio.Transport | None:
+ if self._protocol is None:
+ return None
+ return self._protocol.transport
+
+ @property
+ def writer(self) -> AbstractStreamWriter:
+ return self._payload_writer
+
+ @property
+ def client_max_size(self) -> int:
+ return self._client_max_size
+
+ @reify
+ def message(self) -> RawRequestMessage:
+ warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3)
+ return self._message
+
+ @reify
+ def rel_url(self) -> URL:
+ return self._rel_url
+
+ @reify
+ def loop(self) -> asyncio.AbstractEventLoop:
+ warnings.warn(
+ "request.loop property is deprecated", DeprecationWarning, stacklevel=2
+ )
+ return self._loop
+
+ # MutableMapping API
+
+ @overload # type: ignore[override]
+ def __getitem__(self, key: RequestKey[_T]) -> _T: ...
+
+ @overload
+ def __getitem__(self, key: str) -> Any: ...
+
+ def __getitem__(self, key: str | RequestKey[_T]) -> Any:
+ return self._state[key]
+
+ @overload # type: ignore[override]
+ def __setitem__(self, key: RequestKey[_T], value: _T) -> None: ...
+
+ @overload
+ def __setitem__(self, key: str, value: Any) -> None: ...
+
+ def __setitem__(self, key: str | RequestKey[_T], value: Any) -> None:
+ if not isinstance(key, RequestKey) and key not in BaseRequest._seen_str_keys:
+ BaseRequest._seen_str_keys.add(key)
+ warnings.warn(
+ "It is recommended to use web.RequestKey instances for keys.\n"
+ + "https://docs.aiohttp.org/en/stable/web_advanced.html"
+ + "#request-s-storage",
+ category=NotAppKeyWarning,
+ stacklevel=2,
+ )
+ self._state[key] = value
+
+ def __delitem__(self, key: str | RequestKey[_T]) -> None:
+ del self._state[key]
+
+ def __len__(self) -> int:
+ return len(self._state)
+
+ def __iter__(self) -> Iterator[str | RequestKey[Any]]:
+ return iter(self._state)
+
+ ########
+
+ @reify
+ def secure(self) -> bool:
+ """A bool indicating if the request is handled with SSL."""
+ return self.scheme == "https"
+
+ @reify
+ def forwarded(self) -> tuple[Mapping[str, str], ...]:
+ """A tuple containing all parsed Forwarded header(s).
+
+ Makes an effort to parse Forwarded headers as specified by RFC 7239:
+
+ - It adds one (immutable) dictionary per Forwarded 'field-value', ie
+ per proxy. The element corresponds to the data in the Forwarded
+ field-value added by the first proxy encountered by the client. Each
+ subsequent item corresponds to those added by later proxies.
+ - It checks that every value has valid syntax in general as specified
+ in section 4: either a 'token' or a 'quoted-string'.
+ - It un-escapes found escape sequences.
+ - It does NOT validate 'by' and 'for' contents as specified in section
+ 6.
+ - It does NOT validate 'host' contents (Host ABNF).
+ - It does NOT validate 'proto' contents for valid URI scheme names.
+
+ Returns a tuple containing one or more immutable dicts
+ """
+ elems = []
+ for field_value in self._message.headers.getall(hdrs.FORWARDED, ()):
+ length = len(field_value)
+ pos = 0
+ need_separator = False
+ elem: dict[str, str] = {}
+ elems.append(types.MappingProxyType(elem))
+ while 0 <= pos < length:
+ match = _FORWARDED_PAIR_RE.match(field_value, pos)
+ if match is not None: # got a valid forwarded-pair
+ if need_separator:
+ # bad syntax here, skip to next comma
+ pos = field_value.find(",", pos)
+ else:
+ name, value, port = match.groups()
+ if value[0] == '"':
+ # quoted string: remove quotes and unescape
+ value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1])
+ if port:
+ value += port
+ elem[name.lower()] = value
+ pos += len(match.group(0))
+ need_separator = True
+ elif field_value[pos] == ",": # next forwarded-element
+ need_separator = False
+ elem = {}
+ elems.append(types.MappingProxyType(elem))
+ pos += 1
+ elif field_value[pos] == ";": # next forwarded-pair
+ need_separator = False
+ pos += 1
+ elif field_value[pos] in " \t":
+ # Allow whitespace even between forwarded-pairs, though
+ # RFC 7239 doesn't. This simplifies code and is in line
+ # with Postel's law.
+ pos += 1
+ else:
+ # bad syntax here, skip to next comma
+ pos = field_value.find(",", pos)
+ return tuple(elems)
+
+ @reify
+ def scheme(self) -> str:
+ """A string representing the scheme of the request.
+
+ Hostname is resolved in this order:
+
+ - overridden value by .clone(scheme=new_scheme) call.
+ - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise.
+
+ 'http' or 'https'.
+ """
+ if self._transport_sslcontext:
+ return "https"
+ else:
+ return "http"
+
+ @reify
+ def method(self) -> str:
+ """Read only property for getting HTTP method.
+
+ The value is upper-cased str like 'GET', 'POST', 'PUT' etc.
+ """
+ return self._method
+
+ @reify
+ def version(self) -> HttpVersion:
+ """Read only property for getting HTTP version of request.
+
+ Returns aiohttp.protocol.HttpVersion instance.
+ """
+ return self._version
+
+ @reify
+ def host(self) -> str:
+ """Hostname of the request.
+
+ Hostname is resolved in this order:
+
+ - overridden value by .clone(host=new_host) call.
+ - HOST HTTP header
+ - local socket address the request arrived on
+ (transport ``sockname``)
+ - empty string if no transport information is available
+
+ For example, 'example.com' or 'localhost:8080'.
+
+ For historical reasons, the port number may be included.
+ """
+ host = self._message.headers.get(hdrs.HOST)
+ if host is not None:
+ return host
+ sockname = self._transport_sockname
+ if sockname is None:
+ return ""
+ if isinstance(sockname, tuple):
+ # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid);
+ # bracket the bare address so it matches the Host-header shape
+ # and is a valid URL authority component.
+ if len(sockname) == 4:
+ return f"[{sockname[0]}]"
+ return str(sockname[0])
+ return str(sockname)
+
+ @reify
+ def remote(self) -> str | None:
+ """Remote IP of client initiated HTTP request.
+
+ The IP is resolved in this order:
+
+ - overridden value by .clone(remote=new_remote) call.
+ - peername of opened socket
+ """
+ if self._transport_peername is None:
+ return None
+ if isinstance(self._transport_peername, (list, tuple)):
+ return str(self._transport_peername[0])
+ return str(self._transport_peername)
+
+ @reify
+ def url(self) -> URL:
+ """The full URL of the request."""
+ # authority is used here because it may include the port number
+ # and we want yarl to parse it correctly
+ return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url)
+
+ @reify
+ def path(self) -> str:
+ """The URL including *PATH INFO* without the host or scheme.
+
+ E.g., ``/app/blog``
+ """
+ return self._rel_url.path
+
+ @reify
+ def path_qs(self) -> str:
+ """The URL including PATH_INFO and the query string.
+
+ E.g, /app/blog?id=10
+ """
+ return str(self._rel_url)
+
+ @reify
+ def raw_path(self) -> str:
+ """The URL including raw *PATH INFO* without the host or scheme.
+
+ Warning, the path is unquoted and may contains non valid URL characters
+
+ E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters``
+ """
+ return self._message.path
+
+ @reify
+ def query(self) -> "MultiMapping[str]":
+ """A multidict with all the variables in the query string."""
+ return self._rel_url.query
+
+ @reify
+ def query_string(self) -> str:
+ """The query string in the URL.
+
+ E.g., id=10
+ """
+ return self._rel_url.query_string
+
+ @reify
+ def headers(self) -> CIMultiDictProxy[str]:
+ """A case-insensitive multidict proxy with all headers."""
+ return self._headers
+
+ @reify
+ def raw_headers(self) -> RawHeaders:
+ """A sequence of pairs for all headers."""
+ return self._message.raw_headers
+
+ @reify
+ def if_modified_since(self) -> datetime.datetime | None:
+ """The value of If-Modified-Since HTTP header, or None.
+
+ This header is represented as a `datetime` object.
+ """
+ return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE))
+
+ @reify
+ def if_unmodified_since(self) -> datetime.datetime | None:
+ """The value of If-Unmodified-Since HTTP header, or None.
+
+ This header is represented as a `datetime` object.
+ """
+ return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE))
+
+ @staticmethod
+ def _etag_values(etag_header: str) -> Iterator[ETag]:
+ """Extract `ETag` objects from raw header."""
+ if etag_header == ETAG_ANY:
+ yield ETag(
+ is_weak=False,
+ value=ETAG_ANY,
+ )
+ else:
+ for match in LIST_QUOTED_ETAG_RE.finditer(etag_header):
+ is_weak, value, garbage = match.group(2, 3, 4)
+ # Any symbol captured by 4th group means
+ # that the following sequence is invalid.
+ if garbage:
+ break
+
+ yield ETag(
+ is_weak=bool(is_weak),
+ value=value,
+ )
+
+ @classmethod
+ def _if_match_or_none_impl(
+ cls, header_value: str | None
+ ) -> tuple[ETag, ...] | None:
+ if not header_value:
+ return None
+
+ return tuple(cls._etag_values(header_value))
+
+ @reify
+ def if_match(self) -> tuple[ETag, ...] | None:
+ """The value of If-Match HTTP header, or None.
+
+ This header is represented as a `tuple` of `ETag` objects.
+ """
+ return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH))
+
+ @reify
+ def if_none_match(self) -> tuple[ETag, ...] | None:
+ """The value of If-None-Match HTTP header, or None.
+
+ This header is represented as a `tuple` of `ETag` objects.
+ """
+ return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH))
+
+ @reify
+ def if_range(self) -> datetime.datetime | None:
+ """The value of If-Range HTTP header, or None.
+
+ This header is represented as a `datetime` object.
+ """
+ return parse_http_date(self.headers.get(hdrs.IF_RANGE))
+
+ @reify
+ def keep_alive(self) -> bool:
+ """Is keepalive enabled by client?"""
+ return not self._message.should_close
+
+ @reify
+ def cookies(self) -> Mapping[str, str]:
+ """Return request cookies.
+
+ A read-only dictionary-like object.
+ """
+ # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing
+ # that accepts special characters in cookie names (fixes #2683)
+ parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))
+ # Extract values from Morsel objects
+ return MappingProxyType({name: morsel.value for name, morsel in parsed})
+
+ @reify
+ def http_range(self) -> slice:
+ """The content of Range HTTP header.
+
+ Return a slice instance.
+
+ """
+ rng = self._headers.get(hdrs.RANGE)
+ start, end = None, None
+ if rng is not None:
+ try:
+ pattern = r"^bytes=(\d*)-(\d*)$"
+ start, end = re.findall(pattern, rng, re.ASCII)[0]
+ except IndexError: # pattern was not found in header
+ raise ValueError("range not in acceptable format")
+
+ end = int(end) if end else None
+ start = int(start) if start else None
+
+ if start is None and end is not None:
+ # end with no start is to return tail of content
+ start = -end
+ end = None
+
+ if start is not None and end is not None:
+ # end is inclusive in range header, exclusive for slice
+ end += 1
+
+ if start >= end:
+ raise ValueError("start cannot be after end")
+
+ if start is end is None: # No valid range supplied
+ raise ValueError("No start or end of range specified")
+
+ return slice(start, end, 1)
+
+ @reify
+ def content(self) -> StreamReader:
+ """Return raw payload stream."""
+ return self._payload
+
+ @property
+ def has_body(self) -> bool:
+ """Return True if request's HTTP BODY can be read, False otherwise."""
+ warnings.warn(
+ "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2
+ )
+ return not self._payload.at_eof()
+
+ @property
+ def can_read_body(self) -> bool:
+ """Return True if request's HTTP BODY can be read, False otherwise."""
+ return not self._payload.at_eof()
+
+ @reify
+ def body_exists(self) -> bool:
+ """Return True if request has HTTP BODY, False otherwise."""
+ return type(self._payload) is not EmptyStreamReader
+
+ async def release(self) -> None:
+ """Release request.
+
+ Eat unread part of HTTP BODY if present.
+ """
+ while not self._payload.at_eof():
+ await self._payload.readany()
+
+ async def read(self) -> bytes:
+ """Read request body if present.
+
+ Returns bytes object with full request content.
+ """
+ if self._read_bytes is None:
+ # Raise the buffer limits so compressed payloads decompress in
+ # larger chunks instead of many small pause/resume cycles.
+ if self._client_max_size:
+ self._payload.set_read_chunk_size(self._client_max_size)
+ body = bytearray()
+ while True:
+ chunk = await self._payload.readany()
+ body.extend(chunk)
+ if self._client_max_size:
+ body_size = len(body)
+ if body_size > self._client_max_size:
+ raise HTTPRequestEntityTooLarge(self._client_max_size)
+ if not chunk:
+ break
+ self._read_bytes = bytes(body)
+ return self._read_bytes
+
+ async def text(self) -> str:
+ """Return BODY as text using encoding from .charset."""
+ bytes_body = await self.read()
+ encoding = self.charset or "utf-8"
+ return bytes_body.decode(encoding)
+
+ async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any:
+ """Return BODY as JSON."""
+ body = await self.text()
+ return loads(body)
+
+ async def multipart(self) -> MultipartReader:
+ """Return async iterator to process BODY as multipart."""
+ return MultipartReader(
+ self._headers,
+ self._payload,
+ client_max_size=self._client_max_size,
+ max_field_size=self._protocol.max_field_size,
+ max_headers=self._protocol.max_headers,
+ max_size_error_cls=HTTPRequestEntityTooLarge,
+ )
+
+ async def post(self) -> "MultiDictProxy[_Post]":
+ """Return POST parameters."""
+ if self._post is not None:
+ return self._post
+ if self._method not in self.POST_METHODS:
+ self._post = MultiDictProxy(MultiDict())
+ return self._post
+
+ content_type = self.content_type
+ if content_type not in (
+ "",
+ "application/x-www-form-urlencoded",
+ "multipart/form-data",
+ ):
+ self._post = MultiDictProxy(MultiDict())
+ return self._post
+
+ out: MultiDict[_Post] = MultiDict()
+
+ if content_type == "multipart/form-data":
+ multipart = await self.multipart()
+ max_size = self._client_max_size
+
+ size = 0
+ while (field := await multipart.next()) is not None:
+ field_ct = field.headers.get(hdrs.CONTENT_TYPE)
+
+ if isinstance(field, BodyPartReader):
+ if field.name is None:
+ raise ValueError("Multipart field missing name.")
+
+ # Note that according to RFC 7578, the Content-Type header
+ # is optional, even for files, so we can't assume it's
+ # present.
+ # https://tools.ietf.org/html/rfc7578#section-4.4
+ if field.filename:
+ # store file in temp file
+ tmp = await self._loop.run_in_executor(
+ None, tempfile.TemporaryFile
+ )
+ while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE):
+ async for decoded_chunk in field.decode_iter(chunk):
+ await self._loop.run_in_executor(
+ None, tmp.write, decoded_chunk
+ )
+ size += len(decoded_chunk)
+ if 0 < max_size < size:
+ await self._loop.run_in_executor(None, tmp.close)
+ raise HTTPRequestEntityTooLarge(max_size)
+ await self._loop.run_in_executor(None, tmp.seek, 0)
+
+ if field_ct is None:
+ field_ct = "application/octet-stream"
+
+ ff = FileField(
+ field.name,
+ field.filename,
+ cast(io.BufferedReader, tmp),
+ field_ct,
+ field.headers,
+ )
+ out.add(field.name, ff)
+ else:
+ # deal with ordinary data
+ raw_data = bytearray()
+ while chunk := await field.read_chunk():
+ size += len(chunk)
+ if 0 < max_size < size:
+ raise HTTPRequestEntityTooLarge(max_size)
+ raw_data.extend(chunk)
+
+ value = bytearray()
+ # form-data doesn't support compression, so don't need to check size again.
+ async for d in field.decode_iter(raw_data):
+ value.extend(d)
+
+ if field_ct is None or field_ct.startswith("text/"):
+ charset = field.get_charset(default="utf-8")
+ out.add(field.name, value.decode(charset))
+ else:
+ out.add(field.name, value)
+ else:
+ raise ValueError(
+ "To decode nested multipart you need to use custom reader",
+ )
+ else:
+ data = await self.read()
+ if data:
+ charset = self.charset or "utf-8"
+ out.extend(
+ parse_qsl(
+ data.rstrip().decode(charset),
+ keep_blank_values=True,
+ encoding=charset,
+ )
+ )
+
+ self._post = MultiDictProxy(out)
+ return self._post
+
+ def get_extra_info(self, name: str, default: Any = None) -> Any:
+ """Extra info from protocol transport"""
+ protocol = self._protocol
+ if protocol is None:
+ return default
+
+ transport = protocol.transport
+ if transport is None:
+ return default
+
+ return transport.get_extra_info(name, default)
+
+ def __repr__(self) -> str:
+ ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode(
+ "ascii"
+ )
+ return f"<{self.__class__.__name__} {self._method} {ascii_encodable_path} >"
+
+ def __eq__(self, other: object) -> bool:
+ return id(self) == id(other)
+
+ def __bool__(self) -> bool:
+ return True
+
+ async def _prepare_hook(self, response: StreamResponse) -> None:
+ return
+
+ def _cancel(self, exc: BaseException) -> None:
+ set_exception(self._payload, exc)
+
+ def _finish(self) -> None:
+ if self._post is None or self.content_type != "multipart/form-data":
+ return
+
+ # NOTE: Release file descriptors for the
+ # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom`
+ # NOTE: instances of files sent within multipart request body
+ # NOTE: via HTTP POST request.
+ for file_name, file_field_object in self._post.items():
+ if isinstance(file_field_object, FileField):
+ file_field_object.file.close()
+
+
+class Request(BaseRequest):
+
+ ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"])
+
+ _match_info: Optional["UrlMappingMatchInfo"] = None
+
+ if DEBUG:
+
+ def __setattr__(self, name: str, val: Any) -> None:
+ if name not in self.ATTRS:
+ warnings.warn(
+ f"Setting custom {self.__class__.__name__}.{name} attribute "
+ "is discouraged",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ super().__setattr__(name, val)
+
+ def clone(
+ self,
+ *,
+ method: str | _SENTINEL = sentinel,
+ rel_url: StrOrURL | _SENTINEL = sentinel,
+ headers: LooseHeaders | _SENTINEL = sentinel,
+ scheme: str | _SENTINEL = sentinel,
+ host: str | _SENTINEL = sentinel,
+ remote: str | _SENTINEL = sentinel,
+ client_max_size: int | _SENTINEL = sentinel,
+ ) -> "Request":
+ ret = super().clone(
+ method=method,
+ rel_url=rel_url,
+ headers=headers,
+ scheme=scheme,
+ host=host,
+ remote=remote,
+ client_max_size=client_max_size,
+ )
+ new_ret = cast(Request, ret)
+ new_ret._match_info = self._match_info
+ return new_ret
+
+ @reify
+ def match_info(self) -> "UrlMappingMatchInfo":
+ """Result of route resolving."""
+ match_info = self._match_info
+ assert match_info is not None
+ return match_info
+
+ @property
+ def app(self) -> "Application":
+ """Application instance."""
+ match_info = self._match_info
+ assert match_info is not None
+ return match_info.current_app
+
+ @property
+ def config_dict(self) -> ChainMapProxy:
+ match_info = self._match_info
+ assert match_info is not None
+ lst = match_info.apps
+ app = self.app
+ idx = lst.index(app)
+ sublist = list(reversed(lst[: idx + 1]))
+ return ChainMapProxy(sublist)
+
+ async def _prepare_hook(self, response: StreamResponse) -> None:
+ match_info = self._match_info
+ if match_info is None:
+ return
+ for app in match_info._apps:
+ if on_response_prepare := app.on_response_prepare:
+ await on_response_prepare.send(self, response)
diff --git a/venv/Lib/site-packages/aiohttp/web_response.py b/venv/Lib/site-packages/aiohttp/web_response.py
new file mode 100644
index 0000000000000000000000000000000000000000..23f6bb016ed3a2e4a28dd3fa541324e118cb75eb
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_response.py
@@ -0,0 +1,909 @@
+import asyncio
+import collections.abc
+import datetime
+import enum
+import json
+import math
+import time
+import warnings
+from collections.abc import Iterator, MutableMapping
+from concurrent.futures import Executor
+from http import HTTPStatus
+from http.cookies import SimpleCookie
+from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast, overload
+
+from multidict import CIMultiDict, istr
+
+from . import hdrs, payload
+from .abc import AbstractStreamWriter
+from .compression_utils import MAX_SYNC_CHUNK_SIZE, ZLibCompressor
+from .helpers import (
+ ETAG_ANY,
+ QUOTED_ETAG_RE,
+ ETag,
+ HeadersMixin,
+ ResponseKey,
+ must_be_empty_body,
+ parse_http_date,
+ rfc822_formatted_time,
+ sentinel,
+ should_remove_content_length,
+ validate_etag_value,
+)
+from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11
+from .payload import Payload
+from .typedefs import JSONBytesEncoder, JSONEncoder, LooseHeaders
+
+REASON_PHRASES = {http_status.value: http_status.phrase for http_status in HTTPStatus}
+
+__all__ = (
+ "ContentCoding",
+ "StreamResponse",
+ "Response",
+ "json_response",
+ "json_bytes_response",
+)
+
+
+if TYPE_CHECKING:
+ from .web_request import BaseRequest
+
+ BaseClass = MutableMapping[str, Any]
+else:
+ BaseClass = collections.abc.MutableMapping
+
+
+_T = TypeVar("_T")
+
+
+# TODO(py311): Convert to StrEnum for wider use
+class ContentCoding(enum.Enum):
+ # The content codings that we have support for.
+ #
+ # Additional registered codings are listed at:
+ # https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding
+ deflate = "deflate"
+ gzip = "gzip"
+ identity = "identity"
+
+
+CONTENT_CODINGS = {coding.value: coding for coding in ContentCoding}
+
+############################################################
+# HTTP Response classes
+############################################################
+
+
+class StreamResponse(MutableMapping[str | ResponseKey[Any], Any], HeadersMixin):
+
+ _body: None | bytes | bytearray | Payload
+ _length_check = True
+ _body = None
+ _keep_alive: bool | None = None
+ _chunked: bool = False
+ _compression: bool = False
+ _compression_strategy: int | None = None
+ _compression_force: ContentCoding | None = None
+ _req: Optional["BaseRequest"] = None
+ _payload_writer: AbstractStreamWriter | None = None
+ _eof_sent: bool = False
+ _must_be_empty_body: bool | None = None
+ _body_length = 0
+ _cookies: SimpleCookie | None = None
+ _send_headers_immediately = True
+ _seen_str_keys: set[str] = set()
+
+ def __init__(
+ self,
+ *,
+ status: int = 200,
+ reason: str | None = None,
+ headers: LooseHeaders | None = None,
+ _real_headers: CIMultiDict[str] | None = None,
+ ) -> None:
+ """Initialize a new stream response object.
+
+ _real_headers is an internal parameter used to pass a pre-populated
+ headers object. It is used by the `Response` class to avoid copying
+ the headers when creating a new response object. It is not intended
+ to be used by external code.
+ """
+ self._state: dict[str | ResponseKey[Any], Any] = {}
+
+ if _real_headers is not None:
+ self._headers = _real_headers
+ elif headers is not None:
+ self._headers: CIMultiDict[str] = CIMultiDict(headers)
+ else:
+ self._headers = CIMultiDict()
+
+ self._set_status(status, reason)
+
+ @property
+ def prepared(self) -> bool:
+ return self._eof_sent or self._payload_writer is not None
+
+ @property
+ def task(self) -> "asyncio.Task[None] | None":
+ if self._req:
+ return self._req.task
+ else:
+ return None
+
+ @property
+ def status(self) -> int:
+ return self._status
+
+ @property
+ def chunked(self) -> bool:
+ return self._chunked
+
+ @property
+ def compression(self) -> bool:
+ return self._compression
+
+ @property
+ def reason(self) -> str:
+ return self._reason
+
+ def set_status(
+ self,
+ status: int,
+ reason: str | None = None,
+ ) -> None:
+ assert (
+ not self.prepared
+ ), "Cannot change the response status code after the headers have been sent"
+ self._set_status(status, reason)
+
+ def _set_status(self, status: int, reason: str | None) -> None:
+ self._status = int(status)
+ if reason is None:
+ reason = REASON_PHRASES.get(self._status, "")
+ elif "\r" in reason or "\n" in reason:
+ raise ValueError("Reason cannot contain \\r or \\n")
+ self._reason = reason
+
+ @property
+ def keep_alive(self) -> bool | None:
+ return self._keep_alive
+
+ def force_close(self) -> None:
+ self._keep_alive = False
+
+ @property
+ def body_length(self) -> int:
+ return self._body_length
+
+ @property
+ def output_length(self) -> int:
+ warnings.warn("output_length is deprecated", DeprecationWarning)
+ assert self._payload_writer
+ return self._payload_writer.buffer_size
+
+ def enable_chunked_encoding(self, chunk_size: int | None = None) -> None:
+ """Enables automatic chunked transfer encoding."""
+ if hdrs.CONTENT_LENGTH in self._headers:
+ raise RuntimeError(
+ "You can't enable chunked encoding when a content length is set"
+ )
+ if chunk_size is not None:
+ warnings.warn("Chunk size is deprecated #1615", DeprecationWarning)
+ self._chunked = True
+
+ def enable_compression(
+ self,
+ force: bool | ContentCoding | None = None,
+ strategy: int | None = None,
+ ) -> None:
+ """Enables response compression encoding."""
+ # Backwards compatibility for when force was a bool <0.17.
+ if isinstance(force, bool):
+ force = ContentCoding.deflate if force else ContentCoding.identity
+ warnings.warn(
+ "Using boolean for force is deprecated #3318", DeprecationWarning
+ )
+ elif force is not None:
+ assert isinstance(
+ force, ContentCoding
+ ), "force should one of None, bool or ContentEncoding"
+
+ self._compression = True
+ self._compression_force = force
+ self._compression_strategy = strategy
+
+ @property
+ def headers(self) -> "CIMultiDict[str]":
+ return self._headers
+
+ @property
+ def cookies(self) -> SimpleCookie:
+ if self._cookies is None:
+ self._cookies = SimpleCookie()
+ return self._cookies
+
+ def set_cookie(
+ self,
+ name: str,
+ value: str,
+ *,
+ expires: str | None = None,
+ domain: str | None = None,
+ max_age: int | str | None = None,
+ path: str = "/",
+ secure: bool | None = None,
+ httponly: bool | None = None,
+ version: str | None = None,
+ samesite: str | None = None,
+ partitioned: bool | None = None,
+ ) -> None:
+ """Set or update response cookie.
+
+ Sets new cookie or updates existent with new value.
+ Also updates only those params which are not None.
+ """
+ if self._cookies is None:
+ self._cookies = SimpleCookie()
+
+ self._cookies[name] = value
+ c = self._cookies[name]
+
+ if expires is not None:
+ c["expires"] = expires
+ elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT":
+ del c["expires"]
+
+ if domain is not None:
+ c["domain"] = domain
+
+ if max_age is not None:
+ c["max-age"] = str(max_age)
+ elif "max-age" in c:
+ del c["max-age"]
+
+ c["path"] = path
+
+ if secure is not None:
+ c["secure"] = secure
+ if httponly is not None:
+ c["httponly"] = httponly
+ if version is not None:
+ c["version"] = version
+ if samesite is not None:
+ c["samesite"] = samesite
+
+ if partitioned is not None:
+ c["partitioned"] = partitioned
+
+ def del_cookie(
+ self,
+ name: str,
+ *,
+ domain: str | None = None,
+ path: str = "/",
+ secure: bool | None = None,
+ httponly: bool | None = None,
+ samesite: str | None = None,
+ ) -> None:
+ """Delete cookie.
+
+ Creates new empty expired cookie.
+ """
+ # TODO: do we need domain/path here?
+ if self._cookies is not None:
+ self._cookies.pop(name, None)
+ self.set_cookie(
+ name,
+ "",
+ max_age=0,
+ expires="Thu, 01 Jan 1970 00:00:00 GMT",
+ domain=domain,
+ path=path,
+ secure=secure,
+ httponly=httponly,
+ samesite=samesite,
+ )
+
+ @property
+ def content_length(self) -> int | None:
+ # Just a placeholder for adding setter
+ return super().content_length
+
+ @content_length.setter
+ def content_length(self, value: int | None) -> None:
+ if value is not None:
+ value = int(value)
+ if self._chunked:
+ raise RuntimeError(
+ "You can't set content length when chunked encoding is enable"
+ )
+ self._headers[hdrs.CONTENT_LENGTH] = str(value)
+ else:
+ self._headers.pop(hdrs.CONTENT_LENGTH, None)
+
+ @property
+ def content_type(self) -> str:
+ # Just a placeholder for adding setter
+ return super().content_type
+
+ @content_type.setter
+ def content_type(self, value: str) -> None:
+ self.content_type # read header values if needed
+ self._content_type = str(value)
+ self._generate_content_type_header()
+
+ @property
+ def charset(self) -> str | None:
+ # Just a placeholder for adding setter
+ return super().charset
+
+ @charset.setter
+ def charset(self, value: str | None) -> None:
+ ctype = self.content_type # read header values if needed
+ if ctype == "application/octet-stream":
+ raise RuntimeError(
+ "Setting charset for application/octet-stream "
+ "doesn't make sense, setup content_type first"
+ )
+ assert self._content_dict is not None
+ if value is None:
+ self._content_dict.pop("charset", None)
+ else:
+ self._content_dict["charset"] = str(value).lower()
+ self._generate_content_type_header()
+
+ @property
+ def last_modified(self) -> datetime.datetime | None:
+ """The value of Last-Modified HTTP header, or None.
+
+ This header is represented as a `datetime` object.
+ """
+ return parse_http_date(self._headers.get(hdrs.LAST_MODIFIED))
+
+ @last_modified.setter
+ def last_modified(
+ self, value: int | float | datetime.datetime | str | None
+ ) -> None:
+ if value is None:
+ self._headers.pop(hdrs.LAST_MODIFIED, None)
+ elif isinstance(value, (int, float)):
+ self._headers[hdrs.LAST_MODIFIED] = time.strftime(
+ "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value))
+ )
+ elif isinstance(value, datetime.datetime):
+ self._headers[hdrs.LAST_MODIFIED] = time.strftime(
+ "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple()
+ )
+ elif isinstance(value, str):
+ self._headers[hdrs.LAST_MODIFIED] = value
+ else:
+ msg = f"Unsupported type for last_modified: {type(value).__name__}"
+ raise TypeError(msg)
+
+ @property
+ def etag(self) -> ETag | None:
+ quoted_value = self._headers.get(hdrs.ETAG)
+ if not quoted_value:
+ return None
+ elif quoted_value == ETAG_ANY:
+ return ETag(value=ETAG_ANY)
+ match = QUOTED_ETAG_RE.fullmatch(quoted_value)
+ if not match:
+ return None
+ is_weak, value = match.group(1, 2)
+ return ETag(
+ is_weak=bool(is_weak),
+ value=value,
+ )
+
+ @etag.setter
+ def etag(self, value: ETag | str | None) -> None:
+ if value is None:
+ self._headers.pop(hdrs.ETAG, None)
+ elif (isinstance(value, str) and value == ETAG_ANY) or (
+ isinstance(value, ETag) and value.value == ETAG_ANY
+ ):
+ self._headers[hdrs.ETAG] = ETAG_ANY
+ elif isinstance(value, str):
+ validate_etag_value(value)
+ self._headers[hdrs.ETAG] = f'"{value}"'
+ elif isinstance(value, ETag) and isinstance(value.value, str):
+ validate_etag_value(value.value)
+ hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"'
+ self._headers[hdrs.ETAG] = hdr_value
+ else:
+ raise ValueError(
+ f"Unsupported etag type: {type(value)}. "
+ f"etag must be str, ETag or None"
+ )
+
+ def _generate_content_type_header(
+ self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE
+ ) -> None:
+ assert self._content_dict is not None
+ assert self._content_type is not None
+ params = "; ".join(f"{k}={v}" for k, v in self._content_dict.items())
+ if params:
+ ctype = self._content_type + "; " + params
+ else:
+ ctype = self._content_type
+ self._headers[CONTENT_TYPE] = ctype
+
+ async def _do_start_compression(self, coding: ContentCoding) -> None:
+ if coding is ContentCoding.identity:
+ return
+ assert self._payload_writer is not None
+ self._headers[hdrs.CONTENT_ENCODING] = coding.value
+ self._payload_writer.enable_compression(
+ coding.value, self._compression_strategy
+ )
+ # Compressed payload may have different content length,
+ # remove the header
+ self._headers.popall(hdrs.CONTENT_LENGTH, None)
+
+ async def _start_compression(self, request: "BaseRequest") -> None:
+ if self._compression_force:
+ await self._do_start_compression(self._compression_force)
+ return
+ # Encoding comparisons should be case-insensitive
+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1
+ accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower()
+ for value, coding in CONTENT_CODINGS.items():
+ if value in accept_encoding:
+ await self._do_start_compression(coding)
+ return
+
+ async def prepare(self, request: "BaseRequest") -> AbstractStreamWriter | None:
+ if self._eof_sent:
+ return None
+ if self._payload_writer is not None:
+ return self._payload_writer
+ self._must_be_empty_body = must_be_empty_body(request.method, self.status)
+ return await self._start(request)
+
+ async def _start(self, request: "BaseRequest") -> AbstractStreamWriter:
+ self._req = request
+ writer = self._payload_writer = request._payload_writer
+
+ await self._prepare_headers()
+ await request._prepare_hook(self)
+ await self._write_headers()
+
+ return writer
+
+ async def _prepare_headers(self) -> None:
+ request = self._req
+ assert request is not None
+ writer = self._payload_writer
+ assert writer is not None
+ keep_alive = self._keep_alive
+ if keep_alive is None:
+ keep_alive = request.keep_alive
+ self._keep_alive = keep_alive
+
+ version = request.version
+
+ headers = self._headers
+ if self._cookies:
+ for cookie in self._cookies.values():
+ value = cookie.output(header="")[1:]
+ headers.add(hdrs.SET_COOKIE, value)
+
+ if self._compression:
+ await self._start_compression(request)
+
+ if self._chunked:
+ if version != HttpVersion11:
+ raise RuntimeError(
+ "Using chunked encoding is forbidden "
+ f"for HTTP/{request.version.major}.{request.version.minor}"
+ )
+ if not self._must_be_empty_body:
+ writer.enable_chunking()
+ headers[hdrs.TRANSFER_ENCODING] = "chunked"
+ elif self._length_check: # Disabled for WebSockets
+ writer.length = self.content_length
+ if writer.length is None:
+ if version >= HttpVersion11:
+ if not self._must_be_empty_body:
+ writer.enable_chunking()
+ headers[hdrs.TRANSFER_ENCODING] = "chunked"
+ elif not self._must_be_empty_body:
+ keep_alive = False
+
+ # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2
+ # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4
+ if self._must_be_empty_body:
+ if hdrs.CONTENT_LENGTH in headers and should_remove_content_length(
+ request.method, self.status
+ ):
+ del headers[hdrs.CONTENT_LENGTH]
+ # https://datatracker.ietf.org/doc/html/rfc9112#section-6.1-10
+ # https://datatracker.ietf.org/doc/html/rfc9112#section-6.1-13
+ if hdrs.TRANSFER_ENCODING in headers:
+ del headers[hdrs.TRANSFER_ENCODING]
+ elif (writer.length if self._length_check else self.content_length) != 0:
+ # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5
+ headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream")
+ headers.setdefault(hdrs.DATE, rfc822_formatted_time())
+ headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE)
+
+ # connection header
+ if hdrs.CONNECTION not in headers:
+ if keep_alive:
+ if version == HttpVersion10:
+ headers[hdrs.CONNECTION] = "keep-alive"
+ elif version == HttpVersion11:
+ headers[hdrs.CONNECTION] = "close"
+
+ async def _write_headers(self) -> None:
+ request = self._req
+ assert request is not None
+ writer = self._payload_writer
+ assert writer is not None
+ # status line
+ version = request.version
+ status_line = f"HTTP/{version[0]}.{version[1]} {self._status} {self._reason}"
+ await writer.write_headers(status_line, self._headers)
+ # Send headers immediately if not opted into buffering
+ if self._send_headers_immediately:
+ writer.send_headers()
+
+ async def write(self, data: bytes | bytearray | memoryview) -> None:
+ assert isinstance(
+ data, (bytes, bytearray, memoryview)
+ ), "data argument must be byte-ish (%r)" % type(data)
+
+ if self._eof_sent:
+ raise RuntimeError("Cannot call write() after write_eof()")
+ if self._payload_writer is None:
+ raise RuntimeError("Cannot call write() before prepare()")
+
+ await self._payload_writer.write(data)
+
+ async def drain(self) -> None:
+ assert not self._eof_sent, "EOF has already been sent"
+ assert self._payload_writer is not None, "Response has not been started"
+ warnings.warn(
+ "drain method is deprecated, use await resp.write()",
+ DeprecationWarning,
+ stacklevel=2,
+ )
+ await self._payload_writer.drain()
+
+ async def write_eof(self, data: bytes = b"") -> None:
+ assert isinstance(
+ data, (bytes, bytearray, memoryview)
+ ), "data argument must be byte-ish (%r)" % type(data)
+
+ if self._eof_sent:
+ return
+
+ assert self._payload_writer is not None, "Response has not been started"
+
+ await self._payload_writer.write_eof(data)
+ self._eof_sent = True
+ self._req = None
+ self._body_length = self._payload_writer.output_size
+ self._payload_writer = None
+
+ def __repr__(self) -> str:
+ if self._eof_sent:
+ info = "eof"
+ elif self.prepared:
+ assert self._req is not None
+ info = f"{self._req.method} {self._req.path} "
+ else:
+ info = "not prepared"
+ return f"<{self.__class__.__name__} {self.reason} {info}>"
+
+ @overload # type: ignore[override]
+ def __getitem__(self, key: ResponseKey[_T]) -> _T: ...
+
+ @overload
+ def __getitem__(self, key: str) -> Any: ...
+
+ def __getitem__(self, key: str | ResponseKey[_T]) -> Any:
+ return self._state[key]
+
+ @overload # type: ignore[override]
+ def __setitem__(self, key: ResponseKey[_T], value: _T) -> None: ...
+
+ @overload
+ def __setitem__(self, key: str, value: Any) -> None: ...
+
+ def __setitem__(self, key: str | ResponseKey[_T], value: Any) -> None:
+ if (
+ not isinstance(key, ResponseKey)
+ and key not in StreamResponse._seen_str_keys
+ ):
+ # Import here to break circular dependency
+ from .web_exceptions import NotAppKeyWarning
+
+ StreamResponse._seen_str_keys.add(key)
+ warnings.warn(
+ "It is recommended to use web.ResponseKey instances for keys.\n"
+ + "https://docs.aiohttp.org/en/stable/web_advanced.html"
+ + "#response-s-storage",
+ category=NotAppKeyWarning,
+ stacklevel=2,
+ )
+ self._state[key] = value
+
+ def __delitem__(self, key: str | ResponseKey[_T]) -> None:
+ del self._state[key]
+
+ def __len__(self) -> int:
+ return len(self._state)
+
+ def __iter__(self) -> Iterator[str | ResponseKey[Any]]:
+ return iter(self._state)
+
+ def __hash__(self) -> int:
+ return hash(id(self))
+
+ def __eq__(self, other: object) -> bool:
+ return self is other
+
+ def __bool__(self) -> bool:
+ return True
+
+
+class Response(StreamResponse):
+
+ _compressed_body: bytes | None = None
+ _send_headers_immediately = False
+
+ def __init__(
+ self,
+ *,
+ body: Any = None,
+ status: int = 200,
+ reason: str | None = None,
+ text: str | None = None,
+ headers: LooseHeaders | None = None,
+ content_type: str | None = None,
+ charset: str | None = None,
+ zlib_executor_size: int = MAX_SYNC_CHUNK_SIZE,
+ zlib_executor: Executor | None = None,
+ ) -> None:
+ if body is not None and text is not None:
+ raise ValueError("body and text are not allowed together")
+
+ if headers is None:
+ real_headers: CIMultiDict[str] = CIMultiDict()
+ else:
+ real_headers = CIMultiDict(headers)
+
+ if content_type is not None and "charset" in content_type:
+ raise ValueError("charset must not be in content_type argument")
+
+ if text is not None:
+ if hdrs.CONTENT_TYPE in real_headers:
+ if content_type or charset:
+ raise ValueError(
+ "passing both Content-Type header and "
+ "content_type or charset params "
+ "is forbidden"
+ )
+ else:
+ # fast path for filling headers
+ if not isinstance(text, str):
+ raise TypeError("text argument must be str (%r)" % type(text))
+ if content_type is None:
+ content_type = "text/plain"
+ if charset is None:
+ charset = "utf-8"
+ real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charset
+ body = text.encode(charset)
+ text = None
+ elif hdrs.CONTENT_TYPE in real_headers:
+ if content_type is not None or charset is not None:
+ raise ValueError(
+ "passing both Content-Type header and "
+ "content_type or charset params "
+ "is forbidden"
+ )
+ elif content_type is not None:
+ if charset is not None:
+ content_type += "; charset=" + charset
+ real_headers[hdrs.CONTENT_TYPE] = content_type
+
+ super().__init__(status=status, reason=reason, _real_headers=real_headers)
+
+ if text is not None:
+ self.text = text
+ else:
+ self.body = body
+
+ self._zlib_executor_size = zlib_executor_size
+ self._zlib_executor = zlib_executor
+
+ @property
+ def body(self) -> bytes | bytearray | Payload | None:
+ return self._body
+
+ @body.setter
+ def body(self, body: Any) -> None:
+ if body is None:
+ self._body = None
+ elif isinstance(body, (bytes, bytearray)):
+ self._body = body
+ else:
+ try:
+ self._body = body = payload.PAYLOAD_REGISTRY.get(body)
+ except payload.LookupError:
+ raise ValueError("Unsupported body type %r" % type(body))
+
+ headers = self._headers
+
+ # set content-type
+ if hdrs.CONTENT_TYPE not in headers:
+ headers[hdrs.CONTENT_TYPE] = body.content_type
+
+ # copy payload headers
+ if body.headers:
+ for key, value in body.headers.items():
+ if key not in headers:
+ headers[key] = value
+
+ self._compressed_body = None
+
+ @property
+ def text(self) -> str | None:
+ if self._body is None:
+ return None
+ # Note: When _body is a Payload (e.g. FilePayload), this may do blocking I/O
+ # This is generally safe as most common payloads (BytesPayload, StringPayload)
+ # don't do blocking I/O, but be careful with file-based payloads
+ return self._body.decode(self.charset or "utf-8")
+
+ @text.setter
+ def text(self, text: str) -> None:
+ assert text is None or isinstance(
+ text, str
+ ), "text argument must be str (%r)" % type(text)
+
+ if self.content_type == "application/octet-stream":
+ self.content_type = "text/plain"
+ if self.charset is None:
+ self.charset = "utf-8"
+
+ self._body = text.encode(self.charset)
+ self._compressed_body = None
+
+ @property
+ def content_length(self) -> int | None:
+ if self._chunked:
+ return None
+
+ if hdrs.CONTENT_LENGTH in self._headers:
+ return int(self._headers[hdrs.CONTENT_LENGTH])
+
+ if self._compressed_body is not None:
+ # Return length of the compressed body
+ return len(self._compressed_body)
+ elif isinstance(self._body, Payload):
+ # A payload without content length, or a compressed payload
+ return None
+ elif self._body is not None:
+ return len(self._body)
+ else:
+ return 0
+
+ @content_length.setter
+ def content_length(self, value: int | None) -> None:
+ raise RuntimeError("Content length is set automatically")
+
+ async def write_eof(self, data: bytes = b"") -> None:
+ if self._eof_sent:
+ return
+ if self._compressed_body is None:
+ body = self._body
+ else:
+ body = self._compressed_body
+ assert not data, f"data arg is not supported, got {data!r}"
+ assert self._req is not None
+ assert self._payload_writer is not None
+ if body is None or self._must_be_empty_body:
+ await super().write_eof()
+ elif isinstance(self._body, Payload):
+ try:
+ await self._body.write(self._payload_writer)
+ finally:
+ await self._body.close()
+ await super().write_eof()
+ else:
+ await super().write_eof(cast(bytes, body))
+
+ async def _start(self, request: "BaseRequest") -> AbstractStreamWriter:
+ if hdrs.CONTENT_LENGTH in self._headers:
+ if should_remove_content_length(request.method, self.status):
+ del self._headers[hdrs.CONTENT_LENGTH]
+ elif not self._chunked:
+ if isinstance(self._body, Payload):
+ if (size := self._body.size) is not None:
+ self._headers[hdrs.CONTENT_LENGTH] = str(size)
+ else:
+ body_len = len(self._body) if self._body else "0"
+ # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-7
+ if body_len != "0" or (
+ self.status != 304 and request.method not in hdrs.METH_HEAD_ALL
+ ):
+ self._headers[hdrs.CONTENT_LENGTH] = str(body_len)
+
+ return await super()._start(request)
+
+ async def _do_start_compression(self, coding: ContentCoding) -> None:
+ if self._chunked or isinstance(self._body, Payload):
+ return await super()._do_start_compression(coding)
+ if coding is ContentCoding.identity:
+ return
+ # Instead of using _payload_writer.enable_compression,
+ # compress the whole body
+ compressor = ZLibCompressor(
+ encoding=coding.value,
+ max_sync_chunk_size=self._zlib_executor_size,
+ executor=self._zlib_executor,
+ )
+ assert self._body is not None
+ self._compressed_body = (
+ await compressor.compress(self._body) + compressor.flush()
+ )
+ self._headers[hdrs.CONTENT_ENCODING] = coding.value
+ self._headers[hdrs.CONTENT_LENGTH] = str(len(self._compressed_body))
+
+
+def json_response(
+ data: Any = sentinel,
+ *,
+ text: str | None = None,
+ body: bytes | None = None,
+ status: int = 200,
+ reason: str | None = None,
+ headers: LooseHeaders | None = None,
+ content_type: str = "application/json",
+ dumps: JSONEncoder = json.dumps,
+) -> Response:
+ if data is not sentinel:
+ if text or body:
+ raise ValueError("only one of data, text, or body should be specified")
+ else:
+ text = dumps(data)
+ return Response(
+ text=text,
+ body=body,
+ status=status,
+ reason=reason,
+ headers=headers,
+ content_type=content_type,
+ )
+
+
+def json_bytes_response(
+ data: Any = sentinel,
+ *,
+ dumps: JSONBytesEncoder,
+ body: bytes | None = None,
+ status: int = 200,
+ reason: str | None = None,
+ headers: LooseHeaders | None = None,
+ content_type: str = "application/json",
+) -> Response:
+ """Create a JSON response using a bytes-returning encoder.
+
+ Use this when your JSON encoder (like orjson) returns bytes
+ instead of str, avoiding the encode/decode overhead.
+ """
+ if data is not sentinel:
+ if body is not None:
+ raise ValueError("only one of data or body should be specified")
+ else:
+ body = dumps(data)
+ return Response(
+ body=body,
+ status=status,
+ reason=reason,
+ headers=headers,
+ content_type=content_type,
+ )
diff --git a/venv/Lib/site-packages/aiohttp/web_routedef.py b/venv/Lib/site-packages/aiohttp/web_routedef.py
new file mode 100644
index 0000000000000000000000000000000000000000..66082772af1b55da744b4b110082d9c172f3a9e3
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_routedef.py
@@ -0,0 +1,203 @@
+import abc
+import os # noqa
+from collections.abc import Callable, Iterator, Sequence
+from typing import TYPE_CHECKING, Any, Union, overload
+
+import attr
+
+from . import hdrs
+from .abc import AbstractView
+from .typedefs import Handler, PathLike
+
+if TYPE_CHECKING:
+ from .web_request import Request
+ from .web_response import StreamResponse
+ from .web_urldispatcher import AbstractRoute, UrlDispatcher
+else:
+ Request = StreamResponse = UrlDispatcher = AbstractRoute = None
+
+
+__all__ = (
+ "AbstractRouteDef",
+ "RouteDef",
+ "StaticDef",
+ "RouteTableDef",
+ "head",
+ "options",
+ "get",
+ "post",
+ "patch",
+ "put",
+ "delete",
+ "route",
+ "view",
+ "static",
+)
+
+
+class AbstractRouteDef(abc.ABC):
+ @abc.abstractmethod
+ def register(self, router: UrlDispatcher) -> list[AbstractRoute]:
+ pass # pragma: no cover
+
+
+_HandlerType = Union[type[AbstractView], Handler]
+
+
+@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
+class RouteDef(AbstractRouteDef):
+ method: str
+ path: str
+ handler: _HandlerType
+ kwargs: dict[str, Any]
+
+ def __repr__(self) -> str:
+ info = []
+ for name, value in sorted(self.kwargs.items()):
+ info.append(f", {name}={value!r}")
+ return " {handler.__name__!r}{info}>".format(
+ method=self.method, path=self.path, handler=self.handler, info="".join(info)
+ )
+
+ def register(self, router: UrlDispatcher) -> list[AbstractRoute]:
+ if self.method in hdrs.METH_ALL:
+ reg = getattr(router, "add_" + self.method.lower())
+ return [reg(self.path, self.handler, **self.kwargs)]
+ else:
+ return [
+ router.add_route(self.method, self.path, self.handler, **self.kwargs)
+ ]
+
+
+@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True)
+class StaticDef(AbstractRouteDef):
+ prefix: str
+ path: PathLike
+ kwargs: dict[str, Any]
+
+ def __repr__(self) -> str:
+ info = []
+ for name, value in sorted(self.kwargs.items()):
+ info.append(f", {name}={value!r}")
+ return " {path}{info}>".format(
+ prefix=self.prefix, path=self.path, info="".join(info)
+ )
+
+ def register(self, router: UrlDispatcher) -> list[AbstractRoute]:
+ resource = router.add_static(self.prefix, self.path, **self.kwargs)
+ routes = resource.get_info().get("routes", {})
+ return list(routes.values())
+
+
+def route(method: str, path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return RouteDef(method, path, handler, kwargs)
+
+
+def head(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_HEAD, path, handler, **kwargs)
+
+
+def options(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_OPTIONS, path, handler, **kwargs)
+
+
+def get(
+ path: str,
+ handler: _HandlerType,
+ *,
+ name: str | None = None,
+ allow_head: bool = True,
+ **kwargs: Any,
+) -> RouteDef:
+ return route(
+ hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs
+ )
+
+
+def post(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_POST, path, handler, **kwargs)
+
+
+def put(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_PUT, path, handler, **kwargs)
+
+
+def patch(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_PATCH, path, handler, **kwargs)
+
+
+def delete(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_DELETE, path, handler, **kwargs)
+
+
+def view(path: str, handler: type[AbstractView], **kwargs: Any) -> RouteDef:
+ return route(hdrs.METH_ANY, path, handler, **kwargs)
+
+
+def static(prefix: str, path: PathLike, **kwargs: Any) -> StaticDef:
+ return StaticDef(prefix, path, kwargs)
+
+
+_Deco = Callable[[_HandlerType], _HandlerType]
+
+
+class RouteTableDef(Sequence[AbstractRouteDef]):
+ """Route definition table"""
+
+ def __init__(self) -> None:
+ self._items: list[AbstractRouteDef] = []
+
+ def __repr__(self) -> str:
+ return f""
+
+ @overload
+ def __getitem__(self, index: int) -> AbstractRouteDef: ...
+
+ @overload
+ def __getitem__(self, index: slice) -> list[AbstractRouteDef]: ...
+
+ def __getitem__(self, index): # type: ignore[no-untyped-def]
+ return self._items[index]
+
+ def __iter__(self) -> Iterator[AbstractRouteDef]:
+ return iter(self._items)
+
+ def __len__(self) -> int:
+ return len(self._items)
+
+ def __contains__(self, item: object) -> bool:
+ return item in self._items
+
+ def route(self, method: str, path: str, **kwargs: Any) -> _Deco:
+ def inner(handler: _HandlerType) -> _HandlerType:
+ self._items.append(RouteDef(method, path, handler, kwargs))
+ return handler
+
+ return inner
+
+ def head(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_HEAD, path, **kwargs)
+
+ def get(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_GET, path, **kwargs)
+
+ def post(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_POST, path, **kwargs)
+
+ def put(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_PUT, path, **kwargs)
+
+ def patch(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_PATCH, path, **kwargs)
+
+ def delete(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_DELETE, path, **kwargs)
+
+ def options(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_OPTIONS, path, **kwargs)
+
+ def view(self, path: str, **kwargs: Any) -> _Deco:
+ return self.route(hdrs.METH_ANY, path, **kwargs)
+
+ def static(self, prefix: str, path: PathLike, **kwargs: Any) -> None:
+ self._items.append(StaticDef(prefix, path, kwargs))
diff --git a/venv/Lib/site-packages/aiohttp/web_runner.py b/venv/Lib/site-packages/aiohttp/web_runner.py
new file mode 100644
index 0000000000000000000000000000000000000000..387e692dbe69d54f70da0b26227ae13f5154509a
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_runner.py
@@ -0,0 +1,425 @@
+import asyncio
+import signal
+import socket
+import warnings
+from abc import ABC, abstractmethod
+from typing import TYPE_CHECKING, Any
+
+from yarl import URL
+
+from .abc import AbstractAccessLogger
+from .typedefs import PathLike
+from .web_app import Application
+from .web_log import AccessLogger
+from .web_server import Server
+
+if TYPE_CHECKING:
+ from ssl import SSLContext
+else:
+ try:
+ from ssl import SSLContext
+ except ImportError: # pragma: no cover
+ SSLContext = object # type: ignore[misc,assignment]
+
+__all__ = (
+ "BaseSite",
+ "TCPSite",
+ "UnixSite",
+ "NamedPipeSite",
+ "SockSite",
+ "BaseRunner",
+ "AppRunner",
+ "ServerRunner",
+ "GracefulExit",
+)
+
+
+class GracefulExit(SystemExit):
+ code = 1
+
+
+def _raise_graceful_exit() -> None:
+ raise GracefulExit()
+
+
+class BaseSite(ABC):
+ __slots__ = ("_runner", "_ssl_context", "_backlog", "_server")
+
+ def __init__(
+ self,
+ runner: "BaseRunner",
+ *,
+ shutdown_timeout: float = 60.0,
+ ssl_context: SSLContext | None = None,
+ backlog: int = 128,
+ ) -> None:
+ if runner.server is None:
+ raise RuntimeError("Call runner.setup() before making a site")
+ if shutdown_timeout != 60.0:
+ msg = "shutdown_timeout should be set on BaseRunner"
+ warnings.warn(msg, DeprecationWarning, stacklevel=2)
+ runner._shutdown_timeout = shutdown_timeout
+ self._runner = runner
+ self._ssl_context = ssl_context
+ self._backlog = backlog
+ self._server: asyncio.AbstractServer | None = None
+
+ @property
+ @abstractmethod
+ def name(self) -> str:
+ pass # pragma: no cover
+
+ @abstractmethod
+ async def start(self) -> None:
+ self._runner._reg_site(self)
+
+ async def stop(self) -> None:
+ self._runner._check_site(self)
+ if self._server is not None: # Maybe not started yet
+ self._server.close()
+
+ self._runner._unreg_site(self)
+
+
+class TCPSite(BaseSite):
+ __slots__ = ("_host", "_port", "_bound_port", "_reuse_address", "_reuse_port")
+
+ def __init__(
+ self,
+ runner: "BaseRunner",
+ host: str | None = None,
+ port: int | None = None,
+ *,
+ shutdown_timeout: float = 60.0,
+ ssl_context: SSLContext | None = None,
+ backlog: int = 128,
+ reuse_address: bool | None = None,
+ reuse_port: bool | None = None,
+ ) -> None:
+ super().__init__(
+ runner,
+ shutdown_timeout=shutdown_timeout,
+ ssl_context=ssl_context,
+ backlog=backlog,
+ )
+ self._host = host
+ if port is None:
+ port = 8443 if self._ssl_context else 8080
+ self._port = port
+ self._bound_port: int | None = None
+ self._reuse_address = reuse_address
+ self._reuse_port = reuse_port
+
+ @property
+ def port(self) -> int:
+ """The port the server is listening on.
+
+ If the server hasn't been started yet, this returns the requested port
+ (which might be 0 for a dynamic port).
+ After the server starts, it returns the actual bound port. This is
+ especially useful when port=0 was requested, as it allows retrieving the
+ dynamically assigned port after the site has started.
+ """
+ if self._bound_port is not None:
+ return self._bound_port
+ return self._port
+
+ @property
+ def name(self) -> str:
+ scheme = "https" if self._ssl_context else "http"
+ host = "0.0.0.0" if not self._host else self._host
+ return str(URL.build(scheme=scheme, host=host, port=self.port))
+
+ async def start(self) -> None:
+ await super().start()
+ loop = asyncio.get_event_loop()
+ server = self._runner.server
+ assert server is not None
+ self._server = await loop.create_server(
+ server,
+ self._host,
+ self._port,
+ ssl=self._ssl_context,
+ backlog=self._backlog,
+ reuse_address=self._reuse_address,
+ reuse_port=self._reuse_port,
+ )
+ if self._server.sockets:
+ self._bound_port = self._server.sockets[0].getsockname()[1]
+ else:
+ self._bound_port = self._port
+
+
+class UnixSite(BaseSite):
+ __slots__ = ("_path",)
+
+ def __init__(
+ self,
+ runner: "BaseRunner",
+ path: PathLike,
+ *,
+ shutdown_timeout: float = 60.0,
+ ssl_context: SSLContext | None = None,
+ backlog: int = 128,
+ ) -> None:
+ super().__init__(
+ runner,
+ shutdown_timeout=shutdown_timeout,
+ ssl_context=ssl_context,
+ backlog=backlog,
+ )
+ self._path = path
+
+ @property
+ def name(self) -> str:
+ scheme = "https" if self._ssl_context else "http"
+ return f"{scheme}://unix:{self._path}:"
+
+ async def start(self) -> None:
+ await super().start()
+ loop = asyncio.get_event_loop()
+ server = self._runner.server
+ assert server is not None
+ self._server = await loop.create_unix_server(
+ server,
+ self._path,
+ ssl=self._ssl_context,
+ backlog=self._backlog,
+ )
+
+
+class NamedPipeSite(BaseSite):
+ __slots__ = ("_path",)
+
+ def __init__(
+ self, runner: "BaseRunner", path: str, *, shutdown_timeout: float = 60.0
+ ) -> None:
+ loop = asyncio.get_event_loop()
+ if not isinstance(
+ loop, asyncio.ProactorEventLoop # type: ignore[attr-defined]
+ ):
+ raise RuntimeError(
+ "Named Pipes only available in proactor loop under windows"
+ )
+ super().__init__(runner, shutdown_timeout=shutdown_timeout)
+ self._path = path
+
+ @property
+ def name(self) -> str:
+ return self._path
+
+ async def start(self) -> None:
+ await super().start()
+ loop = asyncio.get_event_loop()
+ server = self._runner.server
+ assert server is not None
+ _server = await loop.start_serving_pipe( # type: ignore[attr-defined]
+ server, self._path
+ )
+ self._server = _server[0]
+
+
+class SockSite(BaseSite):
+ __slots__ = ("_sock", "_name")
+
+ def __init__(
+ self,
+ runner: "BaseRunner",
+ sock: socket.socket,
+ *,
+ shutdown_timeout: float = 60.0,
+ ssl_context: SSLContext | None = None,
+ backlog: int = 128,
+ ) -> None:
+ super().__init__(
+ runner,
+ shutdown_timeout=shutdown_timeout,
+ ssl_context=ssl_context,
+ backlog=backlog,
+ )
+ self._sock = sock
+ scheme = "https" if self._ssl_context else "http"
+ if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX:
+ name = f"{scheme}://unix:{sock.getsockname()}:"
+ else:
+ host, port = sock.getsockname()[:2]
+ name = str(URL.build(scheme=scheme, host=host, port=port))
+ self._name = name
+
+ @property
+ def name(self) -> str:
+ return self._name
+
+ async def start(self) -> None:
+ await super().start()
+ loop = asyncio.get_event_loop()
+ server = self._runner.server
+ assert server is not None
+ self._server = await loop.create_server(
+ server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog
+ )
+
+
+class BaseRunner(ABC):
+ __slots__ = ("_handle_signals", "_kwargs", "_server", "_sites", "_shutdown_timeout")
+
+ def __init__(
+ self,
+ *,
+ handle_signals: bool = False,
+ shutdown_timeout: float = 60.0,
+ **kwargs: Any,
+ ) -> None:
+ self._handle_signals = handle_signals
+ self._kwargs = kwargs
+ self._server: Server | None = None
+ self._sites: list[BaseSite] = []
+ self._shutdown_timeout = shutdown_timeout
+
+ @property
+ def server(self) -> Server | None:
+ return self._server
+
+ @property
+ def addresses(self) -> list[Any]:
+ ret: list[Any] = []
+ for site in self._sites:
+ server = site._server
+ if server is not None:
+ sockets = server.sockets # type: ignore[attr-defined]
+ if sockets is not None:
+ for sock in sockets:
+ ret.append(sock.getsockname())
+ return ret
+
+ @property
+ def sites(self) -> set[BaseSite]:
+ return set(self._sites)
+
+ async def setup(self) -> None:
+ loop = asyncio.get_event_loop()
+
+ if self._handle_signals:
+ try:
+ loop.add_signal_handler(signal.SIGINT, _raise_graceful_exit)
+ loop.add_signal_handler(signal.SIGTERM, _raise_graceful_exit)
+ except NotImplementedError: # pragma: no cover
+ # add_signal_handler is not implemented on Windows
+ pass
+
+ self._server = await self._make_server()
+
+ @abstractmethod
+ async def shutdown(self) -> None:
+ """Call any shutdown hooks to help server close gracefully."""
+
+ async def cleanup(self) -> None:
+ # The loop over sites is intentional, an exception on gather()
+ # leaves self._sites in unpredictable state.
+ # The loop guaranties that a site is either deleted on success or
+ # still present on failure
+ for site in list(self._sites):
+ await site.stop()
+
+ if self._server: # If setup succeeded
+ # Yield to event loop to ensure incoming requests prior to stopping the sites
+ # have all started to be handled before we proceed to close idle connections.
+ await asyncio.sleep(0)
+ self._server.pre_shutdown()
+ await self.shutdown()
+ await self._server.shutdown(self._shutdown_timeout)
+ await self._cleanup_server()
+
+ self._server = None
+ if self._handle_signals:
+ loop = asyncio.get_running_loop()
+ try:
+ loop.remove_signal_handler(signal.SIGINT)
+ loop.remove_signal_handler(signal.SIGTERM)
+ except NotImplementedError: # pragma: no cover
+ # remove_signal_handler is not implemented on Windows
+ pass
+
+ @abstractmethod
+ async def _make_server(self) -> Server:
+ pass # pragma: no cover
+
+ @abstractmethod
+ async def _cleanup_server(self) -> None:
+ pass # pragma: no cover
+
+ def _reg_site(self, site: BaseSite) -> None:
+ if site in self._sites:
+ raise RuntimeError(f"Site {site} is already registered in runner {self}")
+ self._sites.append(site)
+
+ def _check_site(self, site: BaseSite) -> None:
+ if site not in self._sites:
+ raise RuntimeError(f"Site {site} is not registered in runner {self}")
+
+ def _unreg_site(self, site: BaseSite) -> None:
+ if site not in self._sites:
+ raise RuntimeError(f"Site {site} is not registered in runner {self}")
+ self._sites.remove(site)
+
+
+class ServerRunner(BaseRunner):
+ """Low-level web server runner"""
+
+ __slots__ = ("_web_server",)
+
+ def __init__(
+ self, web_server: Server, *, handle_signals: bool = False, **kwargs: Any
+ ) -> None:
+ super().__init__(handle_signals=handle_signals, **kwargs)
+ self._web_server = web_server
+
+ async def shutdown(self) -> None:
+ pass
+
+ async def _make_server(self) -> Server:
+ return self._web_server
+
+ async def _cleanup_server(self) -> None:
+ pass
+
+
+class AppRunner(BaseRunner):
+ """Web Application runner"""
+
+ __slots__ = ("_app",)
+
+ def __init__(
+ self,
+ app: Application,
+ *,
+ handle_signals: bool = False,
+ access_log_class: type[AbstractAccessLogger] = AccessLogger,
+ **kwargs: Any,
+ ) -> None:
+ super().__init__(handle_signals=handle_signals, **kwargs)
+ if not isinstance(app, Application):
+ raise TypeError(
+ f"The first argument should be web.Application instance, got {app!r}"
+ )
+ self._kwargs["access_log_class"] = access_log_class
+ self._app = app
+
+ @property
+ def app(self) -> Application:
+ return self._app
+
+ async def shutdown(self) -> None:
+ await self._app.shutdown()
+
+ async def _make_server(self) -> Server:
+ loop = asyncio.get_event_loop()
+ self._app._set_loop(loop)
+ self._app.on_startup.freeze()
+ await self._app.startup()
+ self._app.freeze()
+
+ return self._app._make_handler(loop=loop, **self._kwargs)
+
+ async def _cleanup_server(self) -> None:
+ await self._app.cleanup()
diff --git a/venv/Lib/site-packages/aiohttp/web_server.py b/venv/Lib/site-packages/aiohttp/web_server.py
new file mode 100644
index 0000000000000000000000000000000000000000..3272c1d78925ff0cf26a232b1fad536e04f7f31d
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_server.py
@@ -0,0 +1,91 @@
+"""Low level HTTP server."""
+
+import asyncio
+from typing import Any, Awaitable, Callable, Dict, List, Optional # noqa
+
+from .abc import AbstractStreamWriter
+from .http_parser import RawRequestMessage
+from .streams import StreamReader
+from .web_protocol import RequestHandler, _RequestFactory, _RequestHandler
+from .web_request import BaseRequest
+
+__all__ = ("Server",)
+
+
+class Server:
+ def __init__(
+ self,
+ handler: _RequestHandler,
+ *,
+ request_factory: _RequestFactory | None = None,
+ handler_cancellation: bool = False,
+ loop: asyncio.AbstractEventLoop | None = None,
+ **kwargs: Any,
+ ) -> None:
+ self._loop = loop or asyncio.get_running_loop()
+ self._connections: dict[RequestHandler, asyncio.Transport] = {}
+ self._kwargs = kwargs
+ # requests_count is the number of requests being processed by the server
+ # for the lifetime of the server.
+ self.requests_count = 0
+ self.request_handler = handler
+ self.request_factory = request_factory or self._make_request
+ self.handler_cancellation = handler_cancellation
+
+ @property
+ def connections(self) -> list[RequestHandler]:
+ return list(self._connections.keys())
+
+ def connection_made(
+ self, handler: RequestHandler, transport: asyncio.Transport
+ ) -> None:
+ self._connections[handler] = transport
+
+ def connection_lost(
+ self, handler: RequestHandler, exc: BaseException | None = None
+ ) -> None:
+ if handler in self._connections:
+ if handler._task_handler:
+ handler._task_handler.add_done_callback(
+ lambda f: self._connections.pop(handler, None)
+ )
+ else:
+ del self._connections[handler]
+
+ def _make_request(
+ self,
+ message: RawRequestMessage,
+ payload: StreamReader,
+ protocol: RequestHandler,
+ writer: AbstractStreamWriter,
+ task: "asyncio.Task[None]",
+ ) -> BaseRequest:
+ return BaseRequest(message, payload, protocol, writer, task, self._loop)
+
+ def pre_shutdown(self) -> None:
+ for conn in self._connections:
+ conn.close()
+
+ async def shutdown(self, timeout: float | None = None) -> None:
+ coros = (conn.shutdown(timeout) for conn in self._connections)
+ await asyncio.gather(*coros)
+ self._connections.clear()
+
+ def __call__(self) -> RequestHandler:
+ try:
+ return RequestHandler(self, loop=self._loop, **self._kwargs)
+ except TypeError:
+ # Failsafe creation: remove all custom handler_args
+ kwargs = {
+ k: v
+ for k, v in self._kwargs.items()
+ if k in ["debug", "access_log_class"]
+ }
+ handler = RequestHandler(self, loop=self._loop, **kwargs)
+ handler.logger.warning(
+ "Failed to create request handler with custom kwargs %r, "
+ "falling back to filtered kwargs. This may indicate a "
+ "misconfiguration.",
+ self._kwargs,
+ )
+ return handler
diff --git a/venv/Lib/site-packages/aiohttp/web_urldispatcher.py b/venv/Lib/site-packages/aiohttp/web_urldispatcher.py
new file mode 100644
index 0000000000000000000000000000000000000000..305dbc3c01400f8e1db51d6a473860ec7a2ab787
--- /dev/null
+++ b/venv/Lib/site-packages/aiohttp/web_urldispatcher.py
@@ -0,0 +1,1281 @@
+import abc
+import asyncio
+import base64
+import functools
+import hashlib
+import html
+import inspect
+import keyword
+import os
+import platform
+import re
+import sys
+import warnings
+from collections.abc import (
+ Awaitable,
+ Callable,
+ Container,
+ Generator,
+ Iterable,
+ Iterator,
+ Mapping,
+ Sized,
+)
+from functools import wraps
+from pathlib import Path
+from re import Pattern
+from types import MappingProxyType
+from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, TypedDict, cast
+
+from yarl import URL, __version__ as yarl_version
+
+from . import hdrs
+from .abc import AbstractMatchInfo, AbstractRouter, AbstractView
+from .helpers import DEBUG, DEFAULT_CHUNK_SIZE
+from .http import HttpVersion11
+from .typedefs import Handler, PathLike
+from .web_exceptions import (
+ HTTPException,
+ HTTPExpectationFailed,
+ HTTPForbidden,
+ HTTPMethodNotAllowed,
+ HTTPNotFound,
+)
+from .web_fileresponse import FileResponse
+from .web_request import Request
+from .web_response import Response, StreamResponse
+from .web_routedef import AbstractRouteDef
+
+__all__ = (
+ "UrlDispatcher",
+ "UrlMappingMatchInfo",
+ "AbstractResource",
+ "Resource",
+ "PlainResource",
+ "DynamicResource",
+ "AbstractRoute",
+ "ResourceRoute",
+ "StaticResource",
+ "View",
+)
+
+
+if TYPE_CHECKING:
+ from .web_app import Application
+
+ BaseDict = dict[str, str]
+else:
+ BaseDict = dict
+
+CIRCULAR_SYMLINK_ERROR = (RuntimeError,) if sys.version_info < (3, 13) else ()
+
+YARL_VERSION: Final[tuple[int, ...]] = tuple(map(int, yarl_version.split(".")[:2]))
+
+HTTP_METHOD_RE: Final[Pattern[str]] = re.compile(
+ r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$"
+)
+ROUTE_RE: Final[Pattern[str]] = re.compile(
+ r"(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})"
+)
+PATH_SEP: Final[str] = re.escape("/")
+
+IS_WINDOWS: Final[bool] = platform.system() == "Windows"
+
+_ExpectHandler = Callable[[Request], Awaitable[StreamResponse | None]]
+_Resolve = tuple[Optional["UrlMappingMatchInfo"], set[str]]
+
+html_escape = functools.partial(html.escape, quote=True)
+
+
+class _InfoDict(TypedDict, total=False):
+ path: str
+
+ formatter: str
+ pattern: Pattern[str]
+
+ directory: Path
+ prefix: str
+ routes: Mapping[str, "AbstractRoute"]
+
+ app: "Application"
+
+ domain: str
+
+ rule: "AbstractRuleMatching"
+
+ http_exception: HTTPException
+
+
+class AbstractResource(Sized, Iterable["AbstractRoute"]):
+ def __init__(self, *, name: str | None = None) -> None:
+ self._name = name
+
+ @property
+ def name(self) -> str | None:
+ return self._name
+
+ @property
+ @abc.abstractmethod
+ def canonical(self) -> str:
+ """Exposes the resource's canonical path.
+
+ For example '/foo/bar/{name}'
+
+ """
+
+ @abc.abstractmethod # pragma: no branch
+ def url_for(self, **kwargs: str) -> URL:
+ """Construct url for resource with additional params."""
+
+ @abc.abstractmethod # pragma: no branch
+ async def resolve(self, request: Request) -> _Resolve:
+ """Resolve resource.
+
+ Return (UrlMappingMatchInfo, allowed_methods) pair.
+ """
+
+ @abc.abstractmethod
+ def add_prefix(self, prefix: str) -> None:
+ """Add a prefix to processed URLs.
+
+ Required for subapplications support.
+ """
+
+ @abc.abstractmethod
+ def get_info(self) -> _InfoDict:
+ """Return a dict with additional info useful for introspection"""
+
+ def freeze(self) -> None:
+ pass
+
+ @abc.abstractmethod
+ def raw_match(self, path: str) -> bool:
+ """Perform a raw match against path"""
+
+
+class AbstractRoute(abc.ABC):
+ def __init__(
+ self,
+ method: str,
+ handler: Handler | type[AbstractView],
+ *,
+ expect_handler: _ExpectHandler | None = None,
+ resource: AbstractResource | None = None,
+ ) -> None:
+
+ if expect_handler is None:
+ expect_handler = _default_expect_handler
+
+ assert inspect.iscoroutinefunction(expect_handler) or (
+ sys.version_info < (3, 14) and asyncio.iscoroutinefunction(expect_handler)
+ ), f"Coroutine is expected, got {expect_handler!r}"
+
+ method = method.upper()
+ if not HTTP_METHOD_RE.match(method):
+ raise ValueError(f"{method} is not allowed HTTP method")
+
+ assert callable(handler), handler
+ if inspect.iscoroutinefunction(handler) or (
+ sys.version_info < (3, 14) and asyncio.iscoroutinefunction(handler)
+ ):
+ pass
+ elif inspect.isgeneratorfunction(handler):
+ if TYPE_CHECKING:
+ assert False
+ warnings.warn(
+ "Bare generators are deprecated, use @coroutine wrapper",
+ DeprecationWarning,
+ )
+ elif isinstance(handler, type) and issubclass(handler, AbstractView):
+ pass
+ else:
+ warnings.warn(
+ "Bare functions are deprecated, use async ones", DeprecationWarning
+ )
+
+ @wraps(handler)
+ async def handler_wrapper(request: Request) -> StreamResponse:
+ result = old_handler(request) # type: ignore[call-arg]
+ if asyncio.iscoroutine(result):
+ result = await result
+ assert isinstance(result, StreamResponse)
+ return result
+
+ old_handler = handler
+ handler = handler_wrapper
+
+ self._method = method
+ self._handler = handler
+ self._expect_handler = expect_handler
+ self._resource = resource
+
+ @property
+ def method(self) -> str:
+ return self._method
+
+ @property
+ def handler(self) -> Handler:
+ return self._handler
+
+ @property
+ @abc.abstractmethod
+ def name(self) -> str | None:
+ """Optional route's name, always equals to resource's name."""
+
+ @property
+ def resource(self) -> AbstractResource | None:
+ return self._resource
+
+ @abc.abstractmethod
+ def get_info(self) -> _InfoDict:
+ """Return a dict with additional info useful for introspection"""
+
+ @abc.abstractmethod # pragma: no branch
+ def url_for(self, *args: str, **kwargs: str) -> URL:
+ """Construct url for route with additional params."""
+
+ async def handle_expect_header(self, request: Request) -> StreamResponse | None:
+ return await self._expect_handler(request)
+
+
+class UrlMappingMatchInfo(BaseDict, AbstractMatchInfo):
+
+ __slots__ = ("_route", "_apps", "_current_app", "_frozen")
+
+ def __init__(self, match_dict: dict[str, str], route: AbstractRoute) -> None:
+ super().__init__(match_dict)
+ self._route = route
+ self._apps: list[Application] = []
+ self._current_app: Application | None = None
+ self._frozen = False
+
+ @property
+ def handler(self) -> Handler:
+ return self._route.handler
+
+ @property
+ def route(self) -> AbstractRoute:
+ return self._route
+
+ @property
+ def expect_handler(self) -> _ExpectHandler:
+ return self._route.handle_expect_header
+
+ @property
+ def http_exception(self) -> HTTPException | None:
+ return None
+
+ def get_info(self) -> _InfoDict: # type: ignore[override]
+ return self._route.get_info()
+
+ @property
+ def apps(self) -> tuple["Application", ...]:
+ return tuple(self._apps)
+
+ def add_app(self, app: "Application") -> None:
+ if self._frozen:
+ raise RuntimeError("Cannot change apps stack after .freeze() call")
+ if self._current_app is None:
+ self._current_app = app
+ self._apps.insert(0, app)
+
+ @property
+ def current_app(self) -> "Application":
+ app = self._current_app
+ assert app is not None
+ return app
+
+ @current_app.setter
+ def current_app(self, app: "Application") -> None:
+ if DEBUG: # pragma: no cover
+ if app not in self._apps:
+ raise RuntimeError(
+ f"Expected one of the following apps {self._apps!r}, got {app!r}"
+ )
+ self._current_app = app
+
+ def freeze(self) -> None:
+ self._frozen = True
+
+ def __repr__(self) -> str:
+ return f""
+
+
+class MatchInfoError(UrlMappingMatchInfo):
+
+ __slots__ = ("_exception",)
+
+ def __init__(self, http_exception: HTTPException) -> None:
+ self._exception = http_exception
+ super().__init__({}, SystemRoute(self._exception))
+
+ @property
+ def http_exception(self) -> HTTPException:
+ return self._exception
+
+ def __repr__(self) -> str:
+ return f""
+
+
+async def _default_expect_handler(request: Request) -> None:
+ """Default handler for Expect header.
+
+ Just send "100 Continue" to client.
+ raise HTTPExpectationFailed if value of header is not "100-continue"
+ """
+ expect = request.headers.get(hdrs.EXPECT, "")
+ if request.version == HttpVersion11:
+ if expect.lower() == "100-continue":
+ await request.writer.write(b"HTTP/1.1 100 Continue\r\n\r\n")
+ # Reset output_size as we haven't started the main body yet.
+ request.writer.output_size = 0
+ else:
+ raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect)
+
+
+class Resource(AbstractResource):
+ def __init__(self, *, name: str | None = None) -> None:
+ super().__init__(name=name)
+ self._routes: dict[str, ResourceRoute] = {}
+ self._any_route: ResourceRoute | None = None
+ self._allowed_methods: set[str] = set()
+
+ def add_route(
+ self,
+ method: str,
+ handler: type[AbstractView] | Handler,
+ *,
+ expect_handler: _ExpectHandler | None = None,
+ ) -> "ResourceRoute":
+ if route := self._routes.get(method, self._any_route):
+ raise RuntimeError(
+ "Added route will never be executed, "
+ f"method {route.method} is already "
+ "registered"
+ )
+
+ route_obj = ResourceRoute(method, handler, self, expect_handler=expect_handler)
+ self.register_route(route_obj)
+ return route_obj
+
+ def register_route(self, route: "ResourceRoute") -> None:
+ assert isinstance(
+ route, ResourceRoute
+ ), f"Instance of Route class is required, got {route!r}"
+ if route.method == hdrs.METH_ANY:
+ self._any_route = route
+ self._allowed_methods.add(route.method)
+ self._routes[route.method] = route
+
+ async def resolve(self, request: Request) -> _Resolve:
+ if (match_dict := self._match(request.rel_url.path_safe)) is None:
+ return None, set()
+ if route := self._routes.get(request.method, self._any_route):
+ return UrlMappingMatchInfo(match_dict, route), self._allowed_methods
+ return None, self._allowed_methods
+
+ @abc.abstractmethod
+ def _match(self, path: str) -> dict[str, str] | None:
+ pass # pragma: no cover
+
+ def __len__(self) -> int:
+ return len(self._routes)
+
+ def __iter__(self) -> Iterator["ResourceRoute"]:
+ return iter(self._routes.values())
+
+ # TODO: implement all abstract methods
+
+
+class PlainResource(Resource):
+ def __init__(self, path: str, *, name: str | None = None) -> None:
+ super().__init__(name=name)
+ assert not path or path.startswith("/")
+ self._path = path
+
+ @property
+ def canonical(self) -> str:
+ return self._path
+
+ def freeze(self) -> None:
+ if not self._path:
+ self._path = "/"
+
+ def add_prefix(self, prefix: str) -> None:
+ assert prefix.startswith("/")
+ assert not prefix.endswith("/")
+ assert len(prefix) > 1
+ self._path = prefix + self._path
+
+ def _match(self, path: str) -> dict[str, str] | None:
+ # string comparison is about 10 times faster than regexp matching
+ if self._path == path:
+ return {}
+ return None
+
+ def raw_match(self, path: str) -> bool:
+ return self._path == path
+
+ def get_info(self) -> _InfoDict:
+ return {"path": self._path}
+
+ def url_for(self) -> URL: # type: ignore[override]
+ return URL.build(path=self._path, encoded=True)
+
+ def __repr__(self) -> str:
+ name = "'" + self.name + "' " if self.name is not None else ""
+ return f""
+
+
+class DynamicResource(Resource):
+
+ DYN = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*)\}")
+ DYN_WITH_RE = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*):(?P.+)\}")
+ GOOD = r"[^{}/]+"
+
+ def __init__(self, path: str, *, name: str | None = None) -> None:
+ super().__init__(name=name)
+ self._orig_path = path
+ pattern = ""
+ formatter = ""
+ for part in ROUTE_RE.split(path):
+ match = self.DYN.fullmatch(part)
+ if match:
+ pattern += "(?P<{}>{})".format(match.group("var"), self.GOOD)
+ formatter += "{" + match.group("var") + "}"
+ continue
+
+ match = self.DYN_WITH_RE.fullmatch(part)
+ if match:
+ pattern += "(?P<{var}>{re})".format(**match.groupdict())
+ formatter += "{" + match.group("var") + "}"
+ continue
+
+ if "{" in part or "}" in part:
+ raise ValueError(f"Invalid path '{path}'['{part}']")
+
+ part = _requote_path(part)
+ formatter += part
+ pattern += re.escape(part)
+
+ try:
+ compiled = re.compile(pattern)
+ except re.error as exc:
+ raise ValueError(f"Bad pattern '{pattern}': {exc}") from None
+ assert compiled.pattern.startswith(PATH_SEP)
+ assert formatter.startswith("/")
+ self._pattern = compiled
+ self._formatter = formatter
+
+ @property
+ def canonical(self) -> str:
+ return self._formatter
+
+ def add_prefix(self, prefix: str) -> None:
+ assert prefix.startswith("/")
+ assert not prefix.endswith("/")
+ assert len(prefix) > 1
+ self._pattern = re.compile(re.escape(prefix) + self._pattern.pattern)
+ self._formatter = prefix + self._formatter
+
+ def _match(self, path: str) -> dict[str, str] | None:
+ match = self._pattern.fullmatch(path)
+ if match is None:
+ return None
+ return {
+ key: _unquote_path_safe(value) for key, value in match.groupdict().items()
+ }
+
+ def raw_match(self, path: str) -> bool:
+ return self._orig_path == path
+
+ def get_info(self) -> _InfoDict:
+ return {"formatter": self._formatter, "pattern": self._pattern}
+
+ def url_for(self, **parts: str) -> URL:
+ url = self._formatter.format_map({k: _quote_path(v) for k, v in parts.items()})
+ return URL.build(path=url, encoded=True)
+
+ def __repr__(self) -> str:
+ name = "'" + self.name + "' " if self.name is not None else ""
+ return f""
+
+
+class PrefixResource(AbstractResource):
+ def __init__(self, prefix: str, *, name: str | None = None) -> None:
+ assert not prefix or prefix.startswith("/"), prefix
+ assert prefix in ("", "/") or not prefix.endswith("/"), prefix
+ super().__init__(name=name)
+ self._prefix = _requote_path(prefix)
+ self._prefix2 = self._prefix + "/"
+
+ @property
+ def canonical(self) -> str:
+ return self._prefix
+
+ def add_prefix(self, prefix: str) -> None:
+ assert prefix.startswith("/")
+ assert not prefix.endswith("/")
+ assert len(prefix) > 1
+ self._prefix = prefix + self._prefix
+ self._prefix2 = self._prefix + "/"
+
+ def raw_match(self, prefix: str) -> bool:
+ return False
+
+ # TODO: impl missing abstract methods
+
+
+class StaticResource(PrefixResource):
+ VERSION_KEY = "v"
+
+ def __init__(
+ self,
+ prefix: str,
+ directory: PathLike,
+ *,
+ name: str | None = None,
+ expect_handler: _ExpectHandler | None = None,
+ chunk_size: int = DEFAULT_CHUNK_SIZE,
+ show_index: bool = False,
+ follow_symlinks: bool = False,
+ append_version: bool = False,
+ ) -> None:
+ super().__init__(prefix, name=name)
+ try:
+ directory = Path(directory).expanduser().resolve(strict=True)
+ except FileNotFoundError as error:
+ raise ValueError(f"'{directory}' does not exist") from error
+ if not directory.is_dir():
+ raise ValueError(f"'{directory}' is not a directory")
+ self._directory = directory
+ self._show_index = show_index
+ self._chunk_size = chunk_size
+ self._follow_symlinks = follow_symlinks
+ self._expect_handler = expect_handler
+ self._append_version = append_version
+
+ self._routes = {
+ "GET": ResourceRoute(
+ "GET", self._handle, self, expect_handler=expect_handler
+ ),
+ "HEAD": ResourceRoute(
+ "HEAD", self._handle, self, expect_handler=expect_handler
+ ),
+ }
+ self._allowed_methods = set(self._routes)
+
+ def url_for( # type: ignore[override]
+ self,
+ *,
+ filename: PathLike,
+ append_version: bool | None = None,
+ ) -> URL:
+ if append_version is None:
+ append_version = self._append_version
+ filename = str(filename).lstrip("/")
+
+ url = URL.build(path=self._prefix, encoded=True)
+ # filename is not encoded
+ if YARL_VERSION < (1, 6):
+ url = url / filename.replace("%", "%25")
+ else:
+ url = url / filename
+
+ if append_version:
+ unresolved_path = self._directory.joinpath(filename)
+ try:
+ if self._follow_symlinks:
+ normalized_path = Path(os.path.normpath(unresolved_path))
+ normalized_path.relative_to(self._directory)
+ filepath = normalized_path.resolve()
+ else:
+ filepath = unresolved_path.resolve()
+ filepath.relative_to(self._directory)
+ except (ValueError, FileNotFoundError):
+ # ValueError for case when path point to symlink
+ # with follow_symlinks is False
+ return url # relatively safe
+ if filepath.is_file():
+ # TODO cache file content
+ # with file watcher for cache invalidation
+ with filepath.open("rb") as f:
+ file_bytes = f.read()
+ h = self._get_file_hash(file_bytes)
+ url = url.with_query({self.VERSION_KEY: h})
+ return url
+ return url
+
+ @staticmethod
+ def _get_file_hash(byte_array: bytes) -> str:
+ m = hashlib.sha256() # todo sha256 can be configurable param
+ m.update(byte_array)
+ b64 = base64.urlsafe_b64encode(m.digest())
+ return b64.decode("ascii")
+
+ def get_info(self) -> _InfoDict:
+ return {
+ "directory": self._directory,
+ "prefix": self._prefix,
+ "routes": self._routes,
+ }
+
+ def set_options_route(self, handler: Handler) -> None:
+ if "OPTIONS" in self._routes:
+ raise RuntimeError("OPTIONS route was set already")
+ self._routes["OPTIONS"] = ResourceRoute(
+ "OPTIONS", handler, self, expect_handler=self._expect_handler
+ )
+ self._allowed_methods.add("OPTIONS")
+
+ async def resolve(self, request: Request) -> _Resolve:
+ path = request.rel_url.path_safe
+ method = request.method
+ # We normalise here to avoid matches that traverse below the static root.
+ # e.g. /static/../../../../home/user/webapp/static/
+ norm_path = os.path.normpath(path)
+ if IS_WINDOWS:
+ norm_path = norm_path.replace("\\", "/")
+ if not norm_path.startswith(self._prefix2) and norm_path != self._prefix:
+ return None, set()
+
+ allowed_methods = self._allowed_methods
+ if method not in allowed_methods:
+ return None, allowed_methods
+
+ match_dict = {"filename": _unquote_path_safe(path[len(self._prefix) + 1 :])}
+ return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods)
+
+ def __len__(self) -> int:
+ return len(self._routes)
+
+ def __iter__(self) -> Iterator[AbstractRoute]:
+ return iter(self._routes.values())
+
+ async def _handle(self, request: Request) -> StreamResponse:
+ filename = request.match_info["filename"]
+ if Path(filename).is_absolute():
+ # filename is an absolute path e.g. //network/share or D:\path
+ # which could be a UNC path leading to NTLM credential theft
+ raise HTTPNotFound()
+ unresolved_path = self._directory.joinpath(filename)
+ loop = asyncio.get_running_loop()
+ return await loop.run_in_executor(
+ None, self._resolve_path_to_response, unresolved_path
+ )
+
+ def _resolve_path_to_response(self, unresolved_path: Path) -> StreamResponse:
+ """Take the unresolved path and query the file system to form a response."""
+ # Check for access outside the root directory. For follow symlinks, URI
+ # cannot traverse out, but symlinks can. Otherwise, no access outside
+ # root is permitted.
+ try:
+ if self._follow_symlinks:
+ normalized_path = Path(os.path.normpath(unresolved_path))
+ normalized_path.relative_to(self._directory)
+ file_path = normalized_path.resolve()
+ else:
+ file_path = unresolved_path.resolve()
+ file_path.relative_to(self._directory)
+ except (ValueError, *CIRCULAR_SYMLINK_ERROR) as error:
+ # ValueError is raised for the relative check. Circular symlinks
+ # raise here on resolving for python < 3.13.
+ raise HTTPNotFound() from error
+
+ # if path is a directory, return the contents if permitted. Note the
+ # directory check will raise if a segment is not readable.
+ try:
+ if file_path.is_dir():
+ if self._show_index:
+ return Response(
+ text=self._directory_as_html(file_path),
+ content_type="text/html",
+ )
+ else:
+ raise HTTPForbidden()
+ except PermissionError as error:
+ raise HTTPForbidden() from error
+
+ # Return the file response, which handles all other checks.
+ return FileResponse(file_path, chunk_size=self._chunk_size)
+
+ def _directory_as_html(self, dir_path: Path) -> str:
+ """returns directory's index as html."""
+ assert dir_path.is_dir()
+
+ relative_path_to_dir = dir_path.relative_to(self._directory).as_posix()
+ index_of = f"Index of /{html_escape(relative_path_to_dir)}"
+ h1 = f"
{index_of}
"
+
+ index_list = []
+ dir_index = dir_path.iterdir()
+ for _file in sorted(dir_index):
+ # show file url as relative to static path
+ rel_path = _file.relative_to(self._directory).as_posix()
+ quoted_file_url = _quote_path(f"{self._prefix}/{rel_path}")
+
+ # if file is a directory, add '/' to the end of the name
+ if _file.is_dir():
+ file_name = f"{_file.name}/"
+ else:
+ file_name = _file.name
+
+ index_list.append(
+ f'