Skip to content

Commit

Permalink
Merge pull request #33 from DavidCEllis/improve_logic
Browse files Browse the repository at this point in the history
Rename ducktools.pyz to ducktools-env.pyz, improve errors, remove .github folder from sdist.
  • Loading branch information
DavidCEllis authored Nov 21, 2024
2 parents a708649 + d6aca3a commit 67616a2
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 176 deletions.
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
prune .github
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,12 +72,15 @@ The tool can be used in multiple ways:
* This adds the `dtrun` shortcut for `ducktools-env run`
* Executed from the zipapp
* Download from: https://github.com/DavidCEllis/ducktools-env/releases/latest
* Run with: `ducktools.pyz <command>`
* Run with: `ducktools-env.pyz <command>`
* The `dtrun.pyz` zipapp is available as a shortcut for `ducktools-env.pyz run`
* Installed in an environment
* Download with `pip` or `uv` in a virtual environment: `pip install ducktools-env`
* Run with: `ducktools-env <command>`
* The `dtrun` shortcut is also available
* Accessed directly via `uvx` with uv
* `uvx ducktools-env <command>`
* No access to the `dtrun` shortcut this way

These examples will use the `ducktools-env` command as the base as if installed via `uv tool` or a similar tool.

Expand Down
318 changes: 184 additions & 134 deletions src/ducktools/env/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ducktools.lazyimporter import LazyImporter, FromImport

from ducktools.env import __version__, PROJECT_NAME
from ducktools.env.exceptions import EnvError

_laz = LazyImporter(
[
Expand Down Expand Up @@ -191,7 +192,7 @@ def get_parser(prog, exit_on_error=True) -> FixedArgumentParser:
create_zipapp_parser.add_argument(
"--zipapp",
action="store_true",
help="Also create the portable ducktools.pyz zipapp",
help="Also create the portable ducktools-env.pyz zipapp",
)

list_parser = subparsers.add_parser(
Expand Down Expand Up @@ -284,9 +285,159 @@ def get_columns(
)


def main():
if __name__ == "__main__":
command = f"{os.path.basename(sys.executable)} -m ducktools.env"
def run_command(manager, args):
# Split on existence of the command as a file, if the file exists run it
# Otherwise look for it in the registered scripts database

if os.path.isfile(args.script_filename):
returncode = manager.run_script(
script_path=args.script_filename,
script_args=args.script_args,
generate_lock=args.generate_lock,
lock_path=args.with_lock,
)
else:
returncode = manager.run_registered_script(
script_name=args.script_filename,
script_args=args.script_args,
generate_lock=args.generate_lock,
lock_path=args.with_lock,
)

return returncode


def bundle_command(manager, args):
manager.create_bundle(
script_path=args.script_filename,
with_lock=args.with_lock,
generate_lock=args.generate_lock,
output_file=args.output,
compressed=args.compress,
)

return 0


def register_command(manager, args):
if args.remove:
# filename should just be the script name, but it's awkward to change this
manager.remove_registered_script(
script_name=args.script_filename,
)
else:
manager.register_script(
script_path=args.script_filename,
script_name=args.name,
)

return 0


def generate_lock_command(manager, args):
lock_path = manager.generate_lockfile(
script_path=args.script_filename,
lockfile_path=args.output,
)
print(f"Lockfile generated at '{lock_path}'")

return 0


def clear_cache_command(manager, args):
if args.full:
manager.clear_project_folder()
else:
manager.clear_temporary_cache()

return 0


def rebuild_env_command(manager, args):
manager.build_env_folder()
if args.zipapp:
manager.build_zipapp()

return 0


def list_command(manager, args):
has_data = False
show_temp = args.temp or not (args.app or args.scripts)
show_app = args.app or not (args.scripts or args.temp)
show_scripts = args.scripts or not (args.app or args.temp)

if (envs := manager.temp_catalogue.environments) and show_temp:
has_data = True
print("Temporary Environments")
print("======================")
formatted = get_columns(
data=envs.values(),
headings=["Name", "Last Used"],
attributes=["name", "last_used_simple"],
)
for line in formatted:
print(line)
if not args.temp:
# newline if not exclusive
print()

if (envs := manager.app_catalogue.environments) and show_app:
has_data = True
print("Application Environments")
print("========================")
formatted = get_columns(
data=envs.values(),
headings=["Owner / Name", "Last Used"],
attributes=["name", "last_used_simple"],
)
for line in formatted:
print(line)
if not args.app:
# newline if not exclusive
print()

if (scripts := manager.script_registry.list_registered_scripts()) and show_scripts:
has_data = True
print("Registered Scripts")
print("==================")

formatted = get_columns(
data=scripts,
headings=["Script Name", "Path"],
attributes=["name", "path"],
)
for line in formatted:
print(line)
print()

if has_data is False:
print("No environments or scripts managed by ducktools-env")

return 0


def delete_env_command(manager, args):
envname = args.environment_name
if envname in manager.temp_catalogue.environments:
manager.temp_catalogue.delete_env(envname)
print(f"Temporary environment {envname!r} deleted")
elif envname in manager.app_catalogue.environments:
manager.app_catalogue.delete_env(envname)
print(f"Application environment {envname!r} deleted")
else:
print(f"Environment {envname!r} not found")

return 0


def main_command() -> int:
executable_name = os.path.splitext(os.path.basename(sys.executable))[0]

if zipapp_path := globals().get("zipapp_path"):
command = f"{executable_name} {zipapp_path}"
elif __name__ == "__main__":
command = f"{executable_name} -m ducktools.env"
else:
command = os.path.basename(sys.argv[0])

Expand All @@ -313,138 +464,37 @@ def main():
command=command,
)

if args.command == "run":
# Split on existence of the command as a file, if the file exists run it
# Otherwise look for it in the registered scripts database
if os.path.isfile(args.script_filename):
manager.run_script(
script_path=args.script_filename,
script_args=args.script_args,
generate_lock=args.generate_lock,
lock_path=args.with_lock,
)
else:
manager.run_registered_script(
script_name=args.script_filename,
script_args=args.script_args,
generate_lock=args.generate_lock,
lock_path=args.with_lock,
)

elif args.command == "bundle":
manager.create_bundle(
script_path=args.script_filename,
with_lock=args.with_lock,
generate_lock=args.generate_lock,
output_file=args.output,
compressed=args.compress,
)

elif args.command == "register":
if args.remove:
# filename should just be the script name, but it's awkward to change this
manager.remove_registered_script(
script_name=args.script_filename,
)
else:
manager.register_script(
script_path=args.script_filename,
script_name=args.name,
)

elif args.command == "generate_lock":
lock_path = manager.generate_lockfile(
script_path=args.script_filename,
lockfile_path=args.output,
)
print(f"Lockfile generated at '{lock_path}'")

elif args.command == "clear_cache":
if args.full:
manager.clear_project_folder()
else:
manager.clear_temporary_cache()

elif args.command == "rebuild_env":
manager.build_env_folder()
if args.zipapp:
manager.build_zipapp()

elif args.command == "list":
has_data = False
show_temp = args.temp or not (args.app or args.scripts)
show_app = args.app or not (args.scripts or args.temp)
show_scripts = args.scripts or not (args.app or args.temp)

if (envs := manager.temp_catalogue.environments) and show_temp:
has_data = True
print("Temporary Environments")
print("======================")
formatted = get_columns(
data=envs.values(),
headings=["Name", "Last Used"],
attributes=["name", "last_used_simple"],
)
for line in formatted:
print(line)
if not args.temp:
# newline if not exclusive
print()

if (envs := manager.app_catalogue.environments) and show_app:
has_data = True
print("Application Environments")
print("========================")
formatted = get_columns(
data=envs.values(),
headings=["Owner / Name", "Last Used"],
attributes=["name", "last_used_simple"],
)
for line in formatted:
print(line)
if not args.app:
# newline if not exclusive
print()

if (scripts := manager.script_registry.list_registered_scripts()) and show_scripts:
has_data = True
print("Registered Scripts")
print("==================")

formatted = get_columns(
data=scripts,
headings=["Script Name", "Path"],
attributes=["name", "path"],
)
for line in formatted:
print(line)
print()

if has_data is False:
print("No environments or scripts managed by ducktools-env")

elif args.command == "delete_env":
envname = args.environment_name
if envname in manager.temp_catalogue.environments:
manager.temp_catalogue.delete_env(envname)
print(f"Temporary environment {envname!r} deleted")
elif envname in manager.app_catalogue.environments:
manager.app_catalogue.delete_env(envname)
print(f"Application environment {envname!r} deleted")
else:
print(f"Environment {envname!r} not found")
else:
# Should be unreachable
raise ValueError("Invalid command")


if __name__ == "__main__":
match args.command:
case "run":
return run_command(manager, args)
case "bundle":
return bundle_command(manager, args)
case "register":
return register_command(manager, args)
case "generate_lock":
return generate_lock_command(manager, args)
case "clear_cache":
return clear_cache_command(manager, args)
case "rebuild_env":
return rebuild_env_command(manager, args)
case "list":
return list_command(manager, args)
case "delete_env":
return delete_env_command(manager, args)
case _:
raise RuntimeError(f"Invalid Command {args.command!r}")


def main() -> int:
try:
main()
except RuntimeError as e:
result = main_command()
except (RuntimeError, EnvError) as e:
errors = "\n".join(e.args) + "\n"
if sys.stderr:
sys.stderr.write(errors)
sys.exit(1)
return 1
return 0

sys.exit(0)

if __name__ == "__main__":
sys.exit(main())
7 changes: 4 additions & 3 deletions src/ducktools/env/_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

from . import PROJECT_NAME
from .manager import Manager
from .exceptions import EnvError


def run():
Expand All @@ -60,10 +61,10 @@ def run():
script_name=app,
script_args=args,
)
except RuntimeError as e:
except (RuntimeError, EnvError) as e:
msg = "\n".join(e.args) + "\n"
if sys.stderr:
sys.stderr.write(msg)
sys.exit(1)
return 1

sys.exit(0)
return 0
Loading

0 comments on commit 67616a2

Please sign in to comment.