Created
February 7, 2025 06:00
-
-
Save stephenhandley/9d9c3c838081c58cddaf06f4efa5073c to your computer and use it in GitHub Desktop.
Script for creating and updating single file python/uv based scripts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env -S uv run --script | |
# /// script | |
# requires-python = ">=3.11" | |
# dependencies = [ | |
# ] | |
# description = "Script for creating and updating single file python/uv based scripts" | |
# /// | |
""" | |
uvscript: Script for creating and updating single file python/uv based scripts | |
Usage: | |
uvscript <name_of_new_or_existing_script> | |
--dependencies <dep1> <dep2> ... | |
--requires-python "<python_version_string>" | |
--description "<script description>" | |
""" | |
import argparse | |
import os | |
import re | |
def generate(dependencies, requires_python, description): | |
header = f"""#!/usr/bin/env -S uv run --script | |
# /// script | |
# requires-python = "{requires_python}" | |
# dependencies = [ | |
""" | |
for dep in dependencies: | |
header += f'# "{dep}",\n' | |
header += "# ]\n" | |
if description: | |
header += f'# description = "{description}"\n' | |
header += "# ///\n\n" | |
# Default main function scaffold with an example argparse argument. | |
desc_text = description if description else "This is a self-contained python executable using uv" | |
main_code = f"""def main(): | |
import argparse | |
import sys | |
parser = argparse.ArgumentParser(description="{desc_text}") | |
parser.add_argument("--example", action="store_true", help="This is an example argument") | |
# Print usage if no arguments are passed | |
if len(sys.argv) == 1: | |
parser.print_usage() | |
sys.exit(1) | |
args = parser.parse_args() | |
if args.example: | |
print("--example argument was used!") | |
if __name__ == "__main__": | |
main() | |
""" | |
return header + main_code | |
def update(contents, dependencies, requires_python, description): | |
""" | |
Update the header block delimited by "# /// script" and "# ///" in the file. | |
If the header block is not found, prepend a full scaffold. | |
""" | |
pattern = re.compile(r'(^# /// script\s+)(.*?)(^# ///\s*$)', re.MULTILINE | re.DOTALL) | |
m = pattern.search(contents) | |
if not m: | |
# If header not found, prepend new scaffold header. | |
return generate(dependencies, requires_python, description) + contents | |
header_block = m.group(2) | |
# Update requires-python line. | |
header_block = re.sub(r'#\s*requires-python\s*=\s*".*?"', f'# requires-python = "{requires_python}"', header_block) | |
# Update dependencies block. | |
new_deps_str = "# dependencies = [\n" | |
for dep in dependencies: | |
new_deps_str += f'# "{dep}",\n' | |
new_deps_str += "# ]" | |
header_block = re.sub(r'#\s*dependencies\s*=\s*\[.*?\n#\s*\]', new_deps_str, header_block, flags=re.DOTALL) | |
# Update description. | |
if description: | |
if re.search(r'#\s*description\s*=', header_block): | |
header_block = re.sub(r'#\s*description\s*=\s*".*?"', f'# description = "{description}"', header_block) | |
else: | |
header_block = header_block.rstrip("\n") + f'\n# description = "{description}"\n' | |
new_header = m.group(1) + header_block + m.group(3) | |
# Replace the original header block with the updated one. | |
new_contents = contents[:m.start()] + new_header + contents[m.end():] | |
# Also update the argparse description in the main code if present. | |
if description: | |
new_contents = re.sub( | |
r'(parser\s*=\s*argparse\.ArgumentParser\(\s*description\s*=\s*")[^"]*(")', | |
r'\1' + description + r'\2', | |
new_contents | |
) | |
return new_contents | |
def main(): | |
parser = argparse.ArgumentParser( | |
description="A tool for creating and updating self-contained executable Python scripts with uv header metadata." | |
) | |
parser.add_argument("script", help="The new or existing script file to create/update.") | |
parser.add_argument("--dependencies", nargs="*", default=[], help="Optional list of dependencies") | |
parser.add_argument("--requires-python", default=">=3.11", help="Optional python version string") | |
parser.add_argument("--description", default="", help="Optional script description") | |
args = parser.parse_args() | |
script_name = args.script | |
dependencies = args.dependencies | |
requires_python = args.requires_python | |
description = args.description | |
if not os.path.exists(script_name): | |
# Generate a new script with the scaffold. | |
contents = generate(dependencies, requires_python, description) | |
with open(script_name, "w") as f: | |
f.write(contents) | |
os.chmod(script_name, 0o755) | |
print(f"Created new script: {script_name}") | |
else: | |
# Update the header in the existing file. | |
with open(script_name, "r") as f: | |
contents = f.read() | |
contents = update(contents, dependencies, requires_python, description) | |
with open(script_name, "w") as f: | |
f.write(contents) | |
print(f"Updated header in existing script: {script_name}") | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment