Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 102 additions & 1 deletion universalpython/universalpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
import sys
import os
import re
import subprocess
import yaml

SCRIPTDIR = os.path.dirname(__file__)
Expand Down Expand Up @@ -56,6 +57,17 @@ def build_alias_map():
DEFAULT_LANGUAGE_MAP = build_language_map()
DEFAULT_ALIAS_MAP = build_alias_map()

UP_FLAGS = {
'-t', '--translate',
'-d', '--dictionary',
'-sl', '--source-language',
'-r', '--reverse',
'-re', '--return',
'-k', '--keep',
'-ko', '--keep-only',
'-o', '--options',
}

def detect_language_from_filename(filename):
"""Detect language from file extension (e.g., my-program.de.py -> german)
Returns tuple: (filepath, two_letter_code) or None"""
Expand Down Expand Up @@ -147,7 +159,93 @@ def run_module(
mod = importlib.import_module(".modes."+mode, package='universalpython')
return mod.run(args, code)

def passthrough_to_python():
"""Replace this process with the real Python interpreter, forwarding all args."""
if os.name == 'nt':
sys.exit(subprocess.call([sys.executable] + sys.argv[1:]))
os.execvp(sys.executable, [sys.executable] + sys.argv[1:])


def print_version():
"""Print UniversalPython version alongside the Python version."""
try:
from importlib.metadata import version as get_version
up_version = get_version("universalpython")
except Exception:
up_version = "dev"
py_version = sys.version.split()[0]
print(f"UniversalPython {up_version} (Python {py_version})")


def print_help():
"""Print UP-specific help, noting that unknown flags pass through to Python."""
print(
"UniversalPython \u2014 Python, but in your native language.\n"
"\n"
"Usage:\n"
" universalpython <file> Compile & run a UniversalPython file\n"
" universalpython --options <file> Compile & run a UP file (explicit)\n"
" universalpython --version | -V Show UP + Python version\n"
" universalpython --help | -h Show this help message\n"
"\n"
"UP compile options:\n"
" -o, --options <file> Compile & run a UniversalPython file\n"
" -t, --translate [engine] Translate identifiers (unidecode | argostranslate)\n"
" -d, --dictionary <path> Path to language dictionary YAML\n"
" -sl, --source-language <code> Source language code (e.g. fr, de)\n"
" -r, --reverse Reverse-translate (English \u2192 target language)\n"
" -re, --return Return compiled code instead of executing\n"
" -k, --keep Save compiled .en.py file and run\n"
" -ko, --keep-only Save compiled .en.py file without running\n"
"\n"
"Anything else (e.g. -c, -m, script.py) is forwarded to Python as-is."

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you run python --help and append its output here (filtering out the ones which conflict with UniversalPython's flags, instead of writing 'anything else'?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D:\UniversalPython>universalpython --help
UniversalPython — Python, but in your native language.

Usage:
universalpython Compile & run a UniversalPython file
universalpython --options Compile & run a UP file (explicit)
universalpython --version | -V Show UP + Python version
universalpython --help | -h Show this help message

UP compile options:
-o, --options Compile & run a UniversalPython file
-t, --translate [engine] Translate identifiers (unidecode | argostranslate)
-d, --dictionary Path to language dictionary YAML
-sl, --source-language Source language code (e.g. fr, de)
-r, --reverse Reverse-translate (English → target language)
-re, --return Return compiled code instead of executing
-k, --keep Save compiled .en.py file and run
-ko, --keep-only Save compiled .en.py file without running

The following Python flags are passed through directly:
Options (and corresponding environment variables):
-b : issue warnings about str(bytes_instance), str(bytearray_instance)
and comparing bytes/bytearray with str. (-bb: issue errors)
-B : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
-c cmd : program passed in as string (terminates option list)
-E : ignore PYTHON* environment variables (such as PYTHONPATH)
-i : inspect interactively after running script; forces a prompt even
if stdin does not appear to be a terminal; also PYTHONINSPECT=x
-I : isolate Python from the user's environment (implies -E and -s)
-m mod : run library module as a script (terminates option list)
-O : remove assert and debug-dependent statements; add .opt-1 before
.pyc extension; also PYTHONOPTIMIZE=x
-OO : do -O changes and also discard docstrings; add .opt-2 before
.pyc extension
-P : don't prepend a potentially unsafe path to sys.path
-q : don't print version and copyright messages on interactive startup
-s : don't add user site directory to sys.path; also PYTHONNOUSERSITE
-S : don't imply 'import site' on initialization
-u : force the stdout and stderr streams to be unbuffered;
this option has no effect on stdin; also PYTHONUNBUFFERED=x
-v : verbose (trace import statements); also PYTHONVERBOSE=x
can be supplied multiple times to increase verbosity
-W arg : warning control; arg is action:message:category:module:lineno
also PYTHONWARNINGS=arg
-x : skip first line of source, allowing use of non-Unix forms of #!cmd
-X opt : set implementation-specific option
--check-hash-based-pycs always|default|never:
control how Python invalidates hash-based .pyc files
--help-env : print help about Python environment variables and exit
--help-xoptions : print help about implementation-specific -X options and exit
--help-all : print complete help information and exit
Arguments:
file : program read from script file

  •  : program read from stdin (default; interactive mode if a tty)
    

arg ...: arguments passed to program in sys.argv[1:]

Is this the output you were expecting?

)


def main():
cli_args = sys.argv[1:]

if not cli_args:
passthrough_to_python()
return

first = cli_args[0]

if first in ('--version', '-V'):
print_version()
return

if first in ('--help', '-h'):
print_help()
return

if first in ('--options', '-o'):
if len(cli_args) < 2:
print("Error: --options requires a filename.")
return
filename = cli_args[1]
try:
with open(filename, encoding='utf-8') as f:
code = f.read()
except FileNotFoundError:
print(f"Error: file not found: {filename}")
return
args = {
'file': [filename],
'translate': False,
'dictionary': "",
'source_language': "",
'reverse': False,
'keep': False,
'keep_only': False,
'return': False,
}
return run_module('lex', code, args)

if first.startswith('-') and first not in UP_FLAGS:
passthrough_to_python()
return

import argparse

# construct the argument parser and parse the argument
Expand Down Expand Up @@ -180,7 +278,7 @@ def main():
help="Translate English code to the language of your choice.")

ap.add_argument("-re", "--return",
action='store_false',
action='store_true',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm... why was this needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad, this must stay store_false, i must have been experimenting with something

default=False, required=False,
help="Return the code instead of executing (used in module mode).")

Expand All @@ -197,6 +295,9 @@ def main():

args = vars(ap.parse_args())

if args['dictionary']:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why was this needed this time? It seems like it might affect compilation ahead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was getting this error: D:\UniversalPython>universalpython test\samples\de\hello.de.py --dictionary languages\de\default.yaml
D:\UniversalPython\universalpython\modes\lex.py:23: RuntimeWarning: Could not load language dictionary file. Defaulting to English Python.
Documentation: https://universalpython.github.io
warnings.warn(
Traceback (most recent call last):
File "", line 198, in _run_module_as_main
File "", line 88, in run_code
File "C:\Users\Sarah\AppData\Local\Programs\Python\Python311\Scripts\universalpython.exe_main
.py", line 6, in
File "D:\UniversalPython\universalpython\universalpython.py", line 308, in main
return run_module(mode, code, args)
File "D:\UniversalPython\universalpython\universalpython.py", line 160, in run_module
return mod.run(args, code)
File "D:\UniversalPython\universalpython\modes\lex.py", line 39, in run
return code if args.get("return") else exec(code)
NameError: name 'ausgeben' is not defined

Before the fix, languages\de\default.yaml was passed as-is to lex.py (a relative path). After the fix, os.path.abspath('languages\de\default.yaml') converts it to D:\UniversalPython\universalpython\languages\de\default.yaml (the actual location of the file) before it reaches lex.py. So lex.py gets a full path

args['dictionary'] = os.path.abspath(args['dictionary'])

filename = args["file"][0]
with open(filename, encoding='utf-8') as code_pyfile:
code = code_pyfile.read()
Expand Down