[Jython-checkins] jython: Re-write main program logic to be more like the CPython main.c (partial #2686)
jeff.allen
jython-checkins at python.org
Sun Sep 23 09:57:15 EDT 2018
https://hg.python.org/jython/rev/9a4866ea2f91
changeset: 8180:9a4866ea2f91
user: Jeff Allen <ja.py at farowl.co.uk>
date: Thu Sep 20 10:53:00 2018 +0100
summary:
Re-write main program logic to be more like the CPython main.c (partial #2686)
This change improves supportability and conformance. It carefully stitches
together existing Jython fragments using a main program logic ported from
CPython 2.7.15. Many a small bug or divergence has been ironed out. Some small
changes were needed outside org.python.util (and a lot of comments). We pass
test_cmd_line_script unmodified and test_cmd_line (updated from CPython)
skipping only -R support.
Support for the -jar option is restored (but deprecated as PEP 338 appears
sufficient). Support for SystemRestart (broken since 2.7.0) is restored only to
be removed shortly -- it can't wholly work and hadn't been missed.
files:
Lib/test/test_cmd_line.py | 129 +-
Lib/test/test_cmd_line_script.py | 224 -
Lib/threading.py | 5 +-
NEWS | 6 +
registry | 8 +
src/org/python/core/Options.java | 57 +-
src/org/python/core/Py.java | 61 +-
src/org/python/core/PySystemState.java | 93 +-
src/org/python/util/OptionScanner.java | 207 +
src/org/python/util/PythonInterpreter.java | 19 +-
src/org/python/util/jython.java | 1354 ++++++---
11 files changed, 1349 insertions(+), 814 deletions(-)
diff --git a/Lib/test/test_cmd_line.py b/Lib/test/test_cmd_line.py
--- a/Lib/test/test_cmd_line.py
+++ b/Lib/test/test_cmd_line.py
@@ -1,37 +1,32 @@
-import os
-import test.test_support, unittest
+# Tests invocation of the interpreter with various command line arguments
+# All tests are executed with environment variables ignored
+# See test_cmd_line_script.py for testing of script execution
+
+import test.test_support
import sys
-import popen2
-import subprocess
+import unittest
+from test.script_helper import (
+ assert_python_ok, assert_python_failure, spawn_python, kill_python,
+ python_exit_code
+)
+
class CmdLineTest(unittest.TestCase):
@classmethod
def tearDownClass(cls):
if test.test_support.is_jython:
- # GC is not immediate, so if Popen.__del__ may be delayed.
+ # GC is not immediate, so Popen.__del__ may be delayed.
# Try to force any Popen.__del__ errors within scope of test.
from test_weakref import extra_collect
extra_collect()
- def start_python(self, cmd_line):
- outfp, infp = popen2.popen4('"%s" %s' % (sys.executable, cmd_line))
- infp.close()
- data = outfp.read()
- outfp.close()
- # try to cleanup the child so we don't appear to leak when running
- # with regrtest -R. This should be a no-op on Windows.
- popen2._cleanup()
- return data
+ def start_python(self, *args):
+ p = spawn_python(*args)
+ return kill_python(p)
def exit_code(self, *args):
- cmd_line = [sys.executable]
- cmd_line.extend(args)
- devnull = open(os.devnull, 'w')
- result = subprocess.call(cmd_line, stdout=devnull,
- stderr=subprocess.STDOUT)
- devnull.close()
- return result
+ return python_exit_code(*args)
def test_directories(self):
self.assertNotEqual(self.exit_code('.'), 0)
@@ -40,10 +35,7 @@
def verify_valid_flag(self, cmd_line):
data = self.start_python(cmd_line)
self.assertTrue(data == '' or data.endswith('\n'))
- self.assertTrue('Traceback' not in data)
-
- def test_environment(self):
- self.verify_valid_flag('-E')
+ self.assertNotIn('Traceback', data)
def test_optimize(self):
self.verify_valid_flag('-O')
@@ -59,14 +51,12 @@
self.verify_valid_flag('-S')
def test_usage(self):
- self.assertTrue('usage' in self.start_python('-h'))
+ self.assertIn('usage', self.start_python('-h'))
def test_version(self):
- prefix = 'J' if test.test_support.is_jython else 'P'
- version = prefix + 'ython %d.%d' % sys.version_info[:2]
- start = self.start_python('-V')
- self.assertTrue(start.startswith(version),
- "%s does not start with %s" % (start, version))
+ prefix = 'Jython' if test.test_support.is_jython else 'Python'
+ version = (prefix + ' %d.%d') % sys.version_info[:2]
+ self.assertTrue(self.start_python('-V').startswith(version))
def test_run_module(self):
# Test expected operation of the '-m' switch
@@ -86,6 +76,17 @@
self.exit_code('-m', 'timeit', '-n', '1'),
0)
+ def test_run_module_bug1764407(self):
+ # -m and -i need to play well together
+ # Runs the timeit module and checks the __main__
+ # namespace has been populated appropriately
+ p = spawn_python('-i', '-m', 'timeit', '-n', '1')
+ p.stdin.write('Timer\n')
+ p.stdin.write('exit()\n')
+ data = kill_python(p)
+ self.assertTrue(data.startswith('1 loop'))
+ self.assertIn('__main__.Timer', data)
+
def test_run_code(self):
# Test expected operation of the '-c' switch
# Switch needs an argument
@@ -99,6 +100,72 @@
self.exit_code('-c', 'pass'),
0)
+ @unittest.skipIf(test.test_support.is_jython,
+ "Hash randomisation is not supported in Jython.")
+ def test_hash_randomization(self):
+ # Verify that -R enables hash randomization:
+ self.verify_valid_flag('-R')
+ hashes = []
+ for i in range(2):
+ code = 'print(hash("spam"))'
+ data = self.start_python('-R', '-c', code)
+ hashes.append(data)
+ self.assertNotEqual(hashes[0], hashes[1])
+
+ # Verify that sys.flags contains hash_randomization
+ code = 'import sys; print sys.flags'
+ data = self.start_python('-R', '-c', code)
+ self.assertTrue('hash_randomization=1' in data)
+
+ def test_del___main__(self):
+ # Issue #15001: PyRun_SimpleFileExFlags() did crash because it kept a
+ # borrowed reference to the dict of __main__ module and later modify
+ # the dict whereas the module was destroyed
+ filename = test.test_support.TESTFN
+ self.addCleanup(test.test_support.unlink, filename)
+ with open(filename, "w") as script:
+ print >>script, "import sys"
+ print >>script, "del sys.modules['__main__']"
+ assert_python_ok(filename)
+
+ def test_unknown_options(self):
+ rc, out, err = assert_python_failure('-E', '-z')
+ self.assertIn(b'Unknown option: -z', err)
+ self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
+ self.assertEqual(b'', out)
+ # Add "without='-E'" to prevent _assert_python to append -E
+ # to env_vars and change the output of stderr
+ rc, out, err = assert_python_failure('-z', without='-E')
+ self.assertIn(b'Unknown option: -z', err)
+ self.assertEqual(err.splitlines().count(b'Unknown option: -z'), 1)
+ self.assertEqual(b'', out)
+ rc, out, err = assert_python_failure('-a', '-z', without='-E')
+ self.assertIn(b'Unknown option: -a', err)
+ # only the first unknown option is reported
+ self.assertNotIn(b'Unknown option: -z', err)
+ self.assertEqual(err.splitlines().count(b'Unknown option: -a'), 1)
+ self.assertEqual(b'', out)
+
+ def test_jython_startup(self):
+ # Test that the file designated by JYTHONSTARTUP is executed when interactive
+ filename = test.test_support.TESTFN
+ self.addCleanup(test.test_support.unlink, filename)
+ with open(filename, "w") as script:
+ print >>script, "print 6*7"
+ print >>script, "print 'Ni!'"
+ expected = ['42', 'Ni!']
+ def check(*args, **kwargs):
+ result = assert_python_ok(*args, **kwargs)
+ self.assertListEqual(expected, result[1].splitlines())
+ if test.test_support.is_jython:
+ # Jython produces a prompt before exit, but not CPython. Hard to say who is correct.
+ expected.append('>>> ')
+ # The Jython way is to set a registry item python.startup
+ check('-i', '-J-Dpython.startup={}'.format(filename))
+ # But a JYTHONSTARTUP environment variable is also supported
+ check('-i', JYTHONSTARTUP=filename)
+ else:
+ check('-i', PYTHONSTARTUP=filename)
def test_main():
test.test_support.run_unittest(CmdLineTest)
diff --git a/Lib/test/test_cmd_line_script.py b/Lib/test/test_cmd_line_script.py
deleted file mode 100644
--- a/Lib/test/test_cmd_line_script.py
+++ /dev/null
@@ -1,224 +0,0 @@
-# Tests command line execution of scripts
-
-import unittest
-import os
-import os.path
-import test.test_support
-from test.script_helper import (run_python,
- temp_dir, make_script, compile_script,
- make_pkg, make_zip_script, make_zip_pkg)
-from test.test_support import is_jython
-
-verbose = test.test_support.verbose
-
-
-test_source = """\
-# Script may be run with optimisation enabled, so don't rely on assert
-# statements being executed
-def assertEqual(lhs, rhs):
- if lhs != rhs:
- raise AssertionError('%r != %r' % (lhs, rhs))
-def assertIdentical(lhs, rhs):
- if lhs is not rhs:
- raise AssertionError('%r is not %r' % (lhs, rhs))
-# Check basic code execution
-result = ['Top level assignment']
-def f():
- result.append('Lower level reference')
-f()
-assertEqual(result, ['Top level assignment', 'Lower level reference'])
-# Check population of magic variables
-assertEqual(__name__, '__main__')
-print '__file__==%r' % __file__
-print '__package__==%r' % __package__
-# Check the sys module
-import sys
-assertIdentical(globals(), sys.modules[__name__].__dict__)
-print 'sys.argv[0]==%r' % sys.argv[0]
-"""
-
-def _make_test_script(script_dir, script_basename, source=test_source):
- return make_script(script_dir, script_basename, source)
-
-def _make_test_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,
- source=test_source, depth=1):
- return make_zip_pkg(zip_dir, zip_basename, pkg_name, script_basename,
- source, depth)
-
-# There's no easy way to pass the script directory in to get
-# -m to work (avoiding that is the whole point of making
-# directories and zipfiles executable!)
-# So we fake it for testing purposes with a custom launch script
-launch_source = """\
-import sys, os.path, runpy
-sys.path.insert(0, %s)
-runpy._run_module_as_main(%r)
-"""
-
-def _make_launch_script(script_dir, script_basename, module_name, path=None):
- if path is None:
- path = "os.path.dirname(__file__)"
- else:
- path = repr(path)
- source = launch_source % (path, module_name)
- return make_script(script_dir, script_basename, source)
-
-class CmdLineTest(unittest.TestCase):
- def _check_script(self, script_name, expected_file,
- expected_argv0, expected_package,
- *cmd_line_switches):
- run_args = cmd_line_switches + (script_name,)
- exit_code, data = run_python(*run_args)
- if verbose:
- print 'Output from test script %r:' % script_name
- print data
- self.assertEqual(exit_code, 0)
- printed_file = '__file__==%r' % str(expected_file)
- printed_argv0 = 'sys.argv[0]==%r' % str(expected_argv0)
- printed_package = '__package__==%r' % (str(expected_package) if expected_package is not None else expected_package)
- if verbose:
- print 'Expected output:'
- print printed_file
- print printed_package
- print printed_argv0
- self.assertIn(printed_file, data)
- self.assertIn(printed_package, data)
- self.assertIn(printed_argv0, data)
-
- def _check_import_error(self, script_name, expected_msg,
- *cmd_line_switches):
- run_args = cmd_line_switches + (script_name,)
- exit_code, data = run_python(*run_args)
- if verbose:
- print 'Output from test script %r:' % script_name
- print data
- print 'Expected output: %r' % expected_msg
- self.assertIn(expected_msg, data)
-
- def test_basic_script(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, 'script')
- self._check_script(script_name, script_name, script_name, None)
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_script_compiled(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, 'script')
- compiled_name = compile_script(script_name)
- os.remove(script_name)
- self._check_script(compiled_name, compiled_name, compiled_name, None)
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_directory(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, '__main__')
- self._check_script(script_dir, script_name, script_dir, '')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_directory_compiled(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, '__main__')
- compiled_name = compile_script(script_name)
- os.remove(script_name)
- self._check_script(script_dir, compiled_name, script_dir, '')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_directory_error(self):
- with temp_dir() as script_dir:
- msg = "can't find '__main__' module in %r" % script_dir
- self._check_import_error(script_dir, msg)
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_zipfile(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, '__main__')
- zip_name, run_name = make_zip_script(script_dir, 'test_zip', script_name)
- self._check_script(zip_name, run_name, zip_name, '')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_zipfile_compiled(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, '__main__')
- compiled_name = compile_script(script_name)
- zip_name, run_name = make_zip_script(script_dir, 'test_zip', compiled_name)
- self._check_script(zip_name, run_name, zip_name, '')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_zipfile_error(self):
- with temp_dir() as script_dir:
- script_name = _make_test_script(script_dir, 'not_main')
- zip_name, run_name = make_zip_script(script_dir, 'test_zip', script_name)
- msg = "can't find '__main__' module in %r" % zip_name
- self._check_import_error(zip_name, msg)
-
- def test_module_in_package(self):
- with temp_dir() as script_dir:
- pkg_dir = os.path.join(script_dir, 'test_pkg')
- make_pkg(pkg_dir)
- script_name = _make_test_script(pkg_dir, 'script')
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.script')
- self._check_script(launch_name, script_name, script_name, 'test_pkg')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_module_in_package_in_zipfile(self):
- with temp_dir() as script_dir:
- zip_name, run_name = _make_test_zip_pkg(script_dir, 'test_zip', 'test_pkg', 'script')
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.script', zip_name)
- self._check_script(launch_name, run_name, run_name, 'test_pkg')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_module_in_subpackage_in_zipfile(self):
- with temp_dir() as script_dir:
- zip_name, run_name = _make_test_zip_pkg(script_dir, 'test_zip', 'test_pkg', 'script', depth=2)
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg.test_pkg.script', zip_name)
- self._check_script(launch_name, run_name, run_name, 'test_pkg.test_pkg')
-
- def test_package(self):
- with temp_dir() as script_dir:
- pkg_dir = os.path.join(script_dir, 'test_pkg')
- make_pkg(pkg_dir)
- script_name = _make_test_script(pkg_dir, '__main__')
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg')
- self._check_script(launch_name, script_name,
- script_name, 'test_pkg')
-
- @unittest.skipIf(is_jython, "FIXME: not working in Jython")
- def test_package_compiled(self):
- with temp_dir() as script_dir:
- pkg_dir = os.path.join(script_dir, 'test_pkg')
- make_pkg(pkg_dir)
- script_name = _make_test_script(pkg_dir, '__main__')
- compiled_name = compile_script(script_name)
- os.remove(script_name)
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg')
- self._check_script(launch_name, compiled_name,
- compiled_name, 'test_pkg')
-
- def test_package_error(self):
- with temp_dir() as script_dir:
- pkg_dir = os.path.join(script_dir, 'test_pkg')
- make_pkg(pkg_dir)
- msg = ("'test_pkg' is a package and cannot "
- "be directly executed")
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg')
- self._check_import_error(launch_name, msg)
-
- def test_package_recursion(self):
- with temp_dir() as script_dir:
- pkg_dir = os.path.join(script_dir, 'test_pkg')
- make_pkg(pkg_dir)
- main_dir = os.path.join(pkg_dir, '__main__')
- make_pkg(main_dir)
- msg = ("Cannot use package as __main__ module; "
- "'test_pkg' is a package and cannot "
- "be directly executed")
- launch_name = _make_launch_script(script_dir, 'launch', 'test_pkg')
- self._check_import_error(launch_name, msg)
-
-
-def test_main():
- test.test_support.run_unittest(CmdLineTest)
- test.test_support.reap_children()
-
-if __name__ == '__main__':
- test_main()
diff --git a/Lib/threading.py b/Lib/threading.py
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -223,9 +223,8 @@
except SystemExit:
pass
except InterruptedException:
- # Quiet InterruptedExceptions if they're caused by
- # _systemrestart
- if not jython.shouldRestart:
+ # Quiet InterruptedExceptions if they're caused by system restart
+ if not _sys._shouldRestart:
raise
except:
# If sys.stderr is no more (most likely from interpreter
diff --git a/NEWS b/NEWS
--- a/NEWS
+++ b/NEWS
@@ -21,6 +21,12 @@
- [ 2650 ] Detail message is not set on PyException from PythonInterpreter
- [ 2403 ] VerifyError when implementing interfaces containing default methods (Java 8)
+ New Features
+ - The main program behaves more like CPython in many small ways, including a more correct
+ treatment of the -i option. This simplifies support, and may also make it unnecessary for
+ users to work around differences from CPython.
+ - python.startup registry property (and JYTHONSTARTUP environment variable) added.
+
Jython 2.7.2a1
Bugs fixed
- [ 2632 ] Handle unicode data appropriately in csv module
diff --git a/registry b/registry
--- a/registry
+++ b/registry
@@ -58,6 +58,14 @@
# behaviour.
python.options.caseok = false
+# Setting this non-empty will drop the interpreter into an interactive session at the end of
+# execution, like adding the -i flag (roughly) or setting the environment variable PYTHONINSPECT
+# during execution.
+#python.inspect = true
+
+# Setting this to a file name will cause that file to be run at the start of each interactive
+# session (but not when dropping in with the -i flag in after a script has run).
+#python.startup = jython-startup.py
# Use this registry entry to control the list of builtin modules; you
# can add, remove, or override builtin modules. The value for this
diff --git a/src/org/python/core/Options.java b/src/org/python/core/Options.java
--- a/src/org/python/core/Options.java
+++ b/src/org/python/core/Options.java
@@ -41,12 +41,29 @@
public static boolean respectJavaAccessibility = true;
/**
- * When false the <code>site.py</code> will not be imported. This is only
- * honored by the command line main class.
+ * When {@code false} the <code>site.py</code> will not be imported. This may be set by the
+ * command line main class ({@code -S} option) or from the registry and is checked in
+ * {@link org.python.util.PythonInterpreter}.
+ *
+ * @see #no_site
*/
public static boolean importSite = true;
/**
+ * When {@code true} the {@code site.py} was not imported. This is may be set by the command
+ * line main class ({@code -S} option) or from the registry. However, in Jython 2,
+ * {@code no_site} is simply the opposite of {@link #importSite}, as the interpreter starts up,
+ * provided for compatibility with the standard Python {@code sys.flags}. Actual control over
+ * the import of the site module in Jython 2, when necessary from Java, is accomplished through
+ * {@link #importSite}.
+ */
+ /*
+ * This should be the standard Python way to control import of the site module. Unfortunately,
+ * importSite is quite old and we cannot rule out use by applications. Correct in Jython 3.
+ */
+ public static boolean no_site = false;
+
+ /**
* Set verbosity to Py.ERROR, Py.WARNING, Py.MESSAGE, Py.COMMENT, or
* Py.DEBUG for varying levels of informative messages from Jython. Normally
* this option is set from the command line.
@@ -54,6 +71,21 @@
public static int verbose = Py.MESSAGE;
/**
+ * Set by the {@code -i} option to the interpreter command, to ask for an interactive session to
+ * start after the script ends. It also allows certain streams to be considered interactive when
+ * {@code isatty} is not available.
+ */
+ public static boolean interactive = false;
+
+ /**
+ * When a script given on the command line finishes, start an interactive interpreter. It is set
+ * {@code true} by the {@code -i} option on the command-line, or programmatically from the
+ * script, and reset to {@code false} just before the interactive session starts. (This session
+ * only actually starts if the console is interactive.)
+ */
+ public static boolean inspect = false;
+
+ /**
* A directory where the dynamically generated classes are written. Nothing is
* ever read from here, it is only for debugging purposes.
*/
@@ -78,25 +110,27 @@
/** Whether -3 (py3k warnings) was enabled via the command line. */
public static boolean py3k_warning = false;
-
+
/** Whether -B (don't write bytecode on import) was enabled via the command line. */
public static boolean dont_write_bytecode = false;
/** Whether -E (ignore environment) was enabled via the command line. */
public static boolean ignore_environment = false;
- //XXX: place holder, not implemented yet.
+ /**
+ * Whether -s (don't add user site directory to {@code sys.path}) was on the command line. The
+ * implementation is mostly in the {@code site} module.
+ */
public static boolean no_user_site = false;
- //XXX: place holder, not implemented yet.
- public static boolean no_site = false;
-
//XXX: place holder
public static int bytes_warning = 0;
- // Corresponds to -O (Python bytecode optimization), -OO (remove docstrings)
- // flags in CPython; it's not clear how Jython should expose its optimization,
- // but this is user visible as of 2.7.
+ /**
+ * Corresponds to -O (Python bytecode optimization), -OO (remove docstrings) flags in CPython.
+ * Jython processes the option and makes it visible as of 2.7, but there is no change of
+ * behaviour in the current version.
+ */
public static int optimize = 0;
/**
@@ -201,7 +235,8 @@
}
Options.sreCacheSpec = getStringOption("sre.cachespec", Options.sreCacheSpec);
-
+ Options.inspect |= getStringOption("inspect", "").length() > 0;
Options.importSite = getBooleanOption("import.site", Options.importSite);
+ Options.no_site = !Options.importSite;
}
}
diff --git a/src/org/python/core/Py.java b/src/org/python/core/Py.java
--- a/src/org/python/core/Py.java
+++ b/src/org/python/core/Py.java
@@ -278,26 +278,50 @@
static void maybeSystemExit(PyException exc) {
if (exc.match(Py.SystemExit)) {
+ // No actual exit here if Options.interactive (-i flag) is in force.
+ handleSystemExit(exc);
+ }
+ }
+
+ /**
+ * Exit the process, if {@value Options#inspect}{@code ==false}, cleaning up the system state.
+ * This exception (normally SystemExit) determines the message, if any, and the
+ * {@code System.exit} status.
+ *
+ * @param exc supplies the message or exit status
+ */
+ static void handleSystemExit(PyException exc) {
+ if (!Options.inspect) {
PyObject value = exc.value;
if (PyException.isExceptionInstance(exc.value)) {
value = value.__findattr__("code");
}
- Py.getSystemState().callExitFunc();
+
+ // Decide exit status and produce message while Jython still works
+ int exitStatus;
if (value instanceof PyInteger) {
- System.exit(((PyInteger) value).getValue());
+ exitStatus = ((PyInteger) value).getValue();
} else {
if (value != Py.None) {
try {
Py.println(value);
- System.exit(1);
+ exitStatus = 1;
} catch (Throwable t) {
- // continue
+ exitStatus = 0;
}
+ } else {
+ exitStatus = 0;
}
- System.exit(0);
}
+
+ // Shut down Jython
+ PySystemState sys = Py.getSystemState();
+ sys.callExitFunc();
+ sys.close();
+ System.exit(exitStatus);
}
}
+
public static PyObject StopIteration;
public static PyException StopIteration(String message) {
@@ -1188,15 +1212,35 @@
return str;
}
- /* Display a PyException and stack trace */
+ /**
+ * Display an exception and stack trace through
+ * {@link #printException(Throwable, PyFrame, PyObject)}.
+ *
+ * @param t to display
+ */
public static void printException(Throwable t) {
printException(t, null, null);
}
+ /**
+ * Display an exception and stack trace through
+ * {@link #printException(Throwable, PyFrame, PyObject)}.
+ *
+ * @param t to display
+ * @param f frame at which to start the stack trace
+ */
public static void printException(Throwable t, PyFrame f) {
printException(t, f, null);
}
+ /**
+ * Display an exception and stack trace. If the exception was {@link Py#SystemExit} <b>and</b>
+ * {@link Options#inspect}{@code ==false}, this will exit the JVM.
+ *
+ * @param t to display
+ * @param f frame at which to start the stack trace
+ * @param file output onto this stream or {@link Py#stderr} if {@code null}
+ */
public static synchronized void printException(Throwable t, PyFrame f,
PyObject file) {
StdoutWrapper stderr = Py.stderr;
@@ -1218,6 +1262,7 @@
PyException exc = Py.JavaError(t);
+ // Act on SystemExit here.
maybeSystemExit(exc);
setException(exc, f);
@@ -1772,6 +1817,8 @@
+ "You can use the -S option or python.import.site=false to not import the site module";
public static boolean importSiteIfSelected() {
+ // Ensure sys.flags.no_site actually reflects what happened. (See docs of these two.)
+ Options.no_site = !Options.importSite;
if (Options.importSite) {
try {
// Ensure site-packages are available
@@ -2670,7 +2717,7 @@
return url;
}
-//------------------------contructor-section---------------------------
+//------------------------constructor-section---------------------------
static class py2JyClassCacheItem {
List<Class<?>> interfaces;
List<PyObject> pyClasses;
diff --git a/src/org/python/core/PySystemState.java b/src/org/python/core/PySystemState.java
--- a/src/org/python/core/PySystemState.java
+++ b/src/org/python/core/PySystemState.java
@@ -233,7 +233,7 @@
currentWorkingDir = new File("").getAbsolutePath();
dont_write_bytecode = Options.dont_write_bytecode;
- py3kwarning = Options.py3k_warning;
+ py3kwarning = Options.py3k_warning; // XXX why here if static?
// Set up the initial standard ins and outs
String mode = Options.unbuffered ? "b" : "";
int buffering = Options.unbuffered ? 0 : 1;
@@ -746,6 +746,15 @@
this.classLoader = classLoader;
}
+ /**
+ * Work out the root directory of the installation of Jython. Sources for this information are
+ * quite diverse. {@code python.home} will take precedence if set in either
+ * {@code postProperties} or {@code preProperties}, {@code install.root} in
+ * {@code preProperties}, in that order. After this, we search the class path for a JAR, or
+ * nagigate from the JAR deduced by from the class path, or finally {@code jarFileName}.
+ * <p>
+ * We also set by side-effect: {@link #defaultPlatform} from {@code java.version}.
+ */
private static String findRoot(Properties preProperties, Properties postProperties,
String jarFileName) {
String root = null;
@@ -793,6 +802,7 @@
}
}
+ /** Set {@link #defaultPlatform} by examination of the {@code java.version} JVM property. */
private static void determinePlatform(Properties props) {
String version = props.getProperty("java.version");
if (version == null) {
@@ -869,6 +879,54 @@
}
}
+ /**
+ * Install the first argument as the application-wide {@link #registry} (a
+ * {@code java.util.Properties} object), merge values from system and local (or user) properties
+ * files, and finally allow values from {@code postProperties} to override. Usually the first
+ * argument is the {@code System.getProperties()}, if were allowed to access it, and therefore
+ * represents definitions made on the command-line. The net precedence order is:
+ * <table>
+ * <tr>
+ * <th>Source</th>
+ * <th>Filled by</th>
+ * </tr>
+ * <tr>
+ * <td>postProperties</td>
+ * <td>Custom {@link JythonInitializer}</td>
+ * </tr>
+ * <tr>
+ * <td>preProperties</td>
+ * <td>Command-line definitions {@code -Dkey=value})</td>
+ * </tr>
+ * <tr>
+ * <td>... preProperties also contain ...</td>
+ * <td>Environment variables via {@link org.python.util.jython}</td>
+ * </tr>
+ * <tr>
+ * <td>[user.home]/.jython</td>
+ * <td>User-specific registry file</td>
+ * </tr>
+ * <tr>
+ * <td>[python.home]/registry</td>
+ * <td>Installation-wide registry file</td>
+ * </tr>
+ * <tr>
+ * <td>Environmental inference</td>
+ * <td>e.g. {@code locale} command for console encoding</td>
+ * </tr>
+ * </table>
+ * <p>
+ * We call {@link Options#setFromRegistry()} to translate certain final values to
+ * application-wide controls. By side-effect, set {@link #prefix} and {@link #exec_prefix} from
+ * {@link #findRoot(Properties, Properties, String)}. If it has not been set otherwise, a
+ * default value for python.console.encoding is derived from the OS environment, via
+ * {@link #getConsoleEncoding(Properties)}.
+ *
+ * @param preProperties initial registry
+ * @param postProperties overriding values
+ * @param standalone default {@code python.cachedir.skip} to true (if not otherwise defined)
+ * @param jarFileName as a clue to the location of the installation
+ */
private static void initRegistry(Properties preProperties, Properties postProperties,
boolean standalone, String jarFileName) {
if (registry != null) {
@@ -901,6 +959,7 @@
PySystemState.exec_prefix = Py.fileSystemEncode(exec_prefix);
}
try {
+ // XXX: Respect or ignore Options.ignore_environment?
String jythonpath = System.getenv("JYTHONPATH");
if (jythonpath != null) {
registry.setProperty("python.path", jythonpath);
@@ -924,7 +983,7 @@
* python.io.encoding is dubious.
*/
if (!registry.containsKey(PYTHON_CONSOLE_ENCODING)) {
- registry.put(PYTHON_CONSOLE_ENCODING, getConsoleEncoding());
+ registry.put(PYTHON_CONSOLE_ENCODING, getConsoleEncoding(registry));
}
// Set up options from registry
@@ -935,17 +994,19 @@
* Try to determine the console encoding from the platform, if necessary using a sub-process to
* enquire. If everything fails, assume UTF-8.
*
+ * @param props in which to look for clues (normally the Jython registry)
* @return the console encoding (and never {@code null})
*/
- private static String getConsoleEncoding() {
+ private static String getConsoleEncoding(Properties props) {
// From Java 8 onwards, the answer may already be to hand in the registry:
- String encoding = System.getProperty("sun.stdout.encoding");
+ String encoding = props.getProperty("sun.stdout.encoding");
+ String os = props.getProperty("os.name");
if (encoding != null) {
return encoding;
- } else if (System.getProperty("os.name").startsWith("Windows")) {
+ } else if (os != null && os.startsWith("Windows")) {
// Go via the Windows code page built-in command "chcp".
String output = getCommandResult("cmd", "/c", "chcp");
/*
@@ -972,8 +1033,8 @@
}
/**
- * Merge the contents of a property file into the registry without overriding any values already
- * set there.
+ * Merge the contents of a property file into the registry, but existing entries with the same
+ * key take precedence.
*
* @param file
*/
@@ -1162,12 +1223,16 @@
// Condition the console
initConsole(registry);
- // Finish up standard Python initialization...
+ /*
+ * Create the first interpreter (which is also the first instance of the sys module) and
+ * cache it as the default state.
+ */
Py.defaultSystemState = new PySystemState();
Py.setSystemState(Py.defaultSystemState);
if (classLoader != null) {
Py.defaultSystemState.setClassLoader(classLoader);
}
+
Py.initClassExceptions(getDefaultBuiltins());
// Make sure that Exception classes have been loaded
@@ -1179,6 +1244,18 @@
return Py.defaultSystemState;
}
+ /**
+ * Reset the global static {@code PySytemState} so that a subsequent call to
+ * {@link #initialize()} will re-create the state. This is only really necessary in the context
+ * of a system restart, but is harmless when shutting down. Using Python after this call is
+ * likely to result in an implicit full static initialisation, or fail badly.
+ */
+ public static void undoInitialize() {
+ Py.defaultSystemState = null;
+ registry = null;
+ initialized = false;
+ }
+
private static PyVersionInfo getVersionInfo() {
String s;
if (Version.PY_RELEASE_LEVEL == 0x0A) {
diff --git a/src/org/python/util/OptionScanner.java b/src/org/python/util/OptionScanner.java
new file mode 100644
--- /dev/null
+++ b/src/org/python/util/OptionScanner.java
@@ -0,0 +1,207 @@
+package org.python.util;
+
+/**
+ * A somewhat general-purpose scanner for command options, based on CPython {@code getopt.c}.
+ */
+class OptionScanner {
+
+ /** Valid options. ':' means expect an argument following. */
+ private final String programOpts; // e.g. in Python "3bBc:dEhiJm:OQ:RsStuUvVW:xX?";
+ /** Index in argv of the arg currently being processed (or about to be started). */
+ private int argIndex = 0;
+ /** Character index within the current element of argv (of the next option to process). */
+ private int optIndex = 0;
+ /** Option argument (where present for returned option). */
+ private String optarg = null;
+ /** Error message (after returning {@link #ERROR}. */
+ private String message = "";
+ /** Original argv passed at reset */
+ private String[] args;
+ /** Return to indicate argument processing is over. */
+ static final char DONE = '\uffff';
+ /** Return to indicate option was not recognised. */
+ static final char ERROR = '\ufffe';
+ /** Return to indicate the next argument is a free-standing argument. */
+ static final char ARGUMENT = '\ufffd';
+
+ /**
+ * Class representing an argument of the long type, where the whole program argument represents
+ * one option, e.g. "--help" or "-version". Such options must start with a '-'. The client
+ * supplies an array of {@code LongSpec} objects to the constructor to define the valid cases.
+ * Long options are recognised before single-letter options are looked for. Note that "-" itself
+ * is treated as a long option (even though it is quite short), returning
+ * {@link OptionScanner#ERROR} if not explicitly defined as a {@code LongSpec}.
+ */
+ static class LongSpec {
+
+ final String key;
+ final char returnValue;
+ final boolean hasArgument;
+
+ /**
+ * Define that the long argument should return a given char value in calls to
+ * {@link OptionScanner#getOption()}, and whether or not an option argument should appear
+ * following it on the command line. This character value need not be the same as any
+ * single-character option, and may be {@link OptionScanner#DONE} (typically for the key
+ * {@code "--"}.
+ *
+ * @param key to match
+ * @param returnValue to return when that matches
+ * @param hasArgument an argument to the option is expected to follow
+ */
+ public LongSpec(String key, char returnValue, boolean hasArgument) {
+ this.key = key;
+ this.returnValue = returnValue;
+ this.hasArgument = hasArgument;
+ }
+
+ /** The same as {@code LongSpec(key, returnValue, false)}. */
+ public LongSpec(String key, char returnValue) {
+ this(key, returnValue, false);
+ }
+ }
+
+ private final LongSpec[] longSpec;
+
+ /**
+ * Create the scanner from command-line arguments, and information about the valid options.
+ *
+ * @param args command-line arguments (which must not change during scanning)
+ * @param programOpts the one-letter options (with : indicating an option argument
+ * @param longSpec table of long options (like --help)
+ */
+ OptionScanner(String[] args, String programOpts, LongSpec[] longSpec) {
+ this.args = args;
+ this.programOpts = programOpts;
+ this.longSpec = longSpec;
+ }
+
+ /**
+ * Get the next option (as a character), or return a code designating successful or erroneous
+ * completion.
+ *
+ * @return next option from command line: the actual character or a code.
+ */
+ char getOption() {
+ message = "";
+ String arg;
+ optarg = null;
+
+ if (argIndex >= args.length) {
+ // Option processing is complete
+ return DONE;
+ } else {
+ // We are currently processing:
+ arg = args[argIndex];
+ if (optIndex == 0) {
+ // And we're at the start of it.
+ if (!arg.startsWith("-") || arg.length() <= 1) {
+ // Non-option program argument e.g. "-" or file name. Note no ++argIndex.
+ return ARGUMENT;
+ } else if (longSpec != null) {
+ // Test for "whole arg" special cases
+ for (LongSpec spec : longSpec) {
+ if (spec.key.equals(arg)) {
+ if (spec.hasArgument) {
+ // Argument to option should be in next arg
+ if (++argIndex < args.length) {
+ optarg = args[argIndex];
+ } else {
+ // There wasn't a next arg.
+ return error("Argument expected for the %s option", arg);
+ }
+ }
+ // And the next processing will be in the next arg
+ ++argIndex;
+ return spec.returnValue;
+ }
+ }
+ // No match: fall through.
+ }
+ // arg is one or more single character options. Continue after the '-'.
+ optIndex = 1;
+ }
+ }
+
+ // We are in arg=argv[argvIndex] at the character to examine is at optIndex.
+ assert argIndex < args.length;
+ assert optIndex > 0;
+ assert optIndex < arg.length();
+
+ char option = arg.charAt(optIndex++);
+ if (optIndex >= arg.length()) {
+ // The option was at the end of the arg, so the next action uses the next arg.
+ ++argIndex;
+ optIndex = 0;
+ }
+
+ // Look up the option character in the list of allowable ones
+ int ptr;
+ if ((ptr = programOpts.indexOf(option)) < 0 || option == ':') {
+ if (arg.length() <= 2) {
+ return error("Unknown option: -%c", option);
+ } else {
+ // Might be unrecognised long arg, or a one letter option in a group.
+ return error("Unknown option: -%c or '%s'", option, arg);
+ }
+ }
+
+ // Is the option marked as expecting an argument?
+ if (++ptr < programOpts.length() && programOpts.charAt(ptr) == ':') {
+ /*
+ * The option's argument is the rest of the current argv[argvIndex][optIndex:]. If the
+ * option is the last character of arg, argvIndex has already moved on and optIndex==0,
+ * so this statement is still true, except that argvIndex may have moved beyond the end
+ * of the array.
+ */
+ if (argIndex < args.length) {
+ optarg = args[argIndex].substring(optIndex);
+ // And the next processing will be in the next arg
+ ++argIndex;
+ optIndex = 0;
+ } else {
+ // We were looking for an argument but there wasn't one.
+ return error("Argument expected for the -%c option", option);
+ }
+ }
+
+ return option;
+ }
+
+ /** Get the argument of the previously returned option or {@code null} if none. */
+ String getOptionArgument() {
+ return optarg;
+ }
+
+ /**
+ * Get a whole argument (not the argument of an option), for use after {@code ARGUMENT} was
+ * returned. This advances the internal state to the next argument.
+ */
+ String getWholeArgument() {
+ return args[argIndex++];
+ }
+
+ /**
+ * Peek at a whole argument (not the argument of an option), for use after {@code ARGUMENT} was
+ * returned. This <b>does not</b> advance the internal state to the next argument.
+ */
+ String peekWholeArgument() {
+ return args[argIndex];
+ }
+
+ /** Number of arguments that remain unprocessed from the original array. */
+ int countRemainingArguments() {
+ return args.length - argIndex;
+ }
+
+ /** Get the error message (when we previously returned {@link #ERROR}. */
+ String getMessage() {
+ return message;
+ }
+
+ /** Set the error message as {@code String.format(message, args)} and return {@link #ERROR}. */
+ char error(String message, Object... args) {
+ this.message = String.format(message, args);
+ return ERROR;
+ }
+}
diff --git a/src/org/python/util/PythonInterpreter.java b/src/org/python/util/PythonInterpreter.java
--- a/src/org/python/util/PythonInterpreter.java
+++ b/src/org/python/util/PythonInterpreter.java
@@ -6,6 +6,7 @@
import java.util.Properties;
import org.python.antlr.base.mod;
+import org.python.core.CodeFlag;
import org.python.core.CompileMode;
import org.python.core.CompilerFlags;
import org.python.core.imp;
@@ -96,21 +97,19 @@
protected PythonInterpreter(PyObject dict, PySystemState systemState,
boolean useThreadLocalState) {
- if (dict == null) {
- dict = Py.newStringMap();
- }
- globals = dict;
- if (systemState == null) {
- systemState = Py.getSystemState();
- }
- this.systemState = systemState;
+ globals = dict != null ? dict : Py.newStringMap();
+ this.systemState = systemState != null ? systemState : Py.getSystemState();
setSystemState();
this.useThreadLocalState = useThreadLocalState;
if (!useThreadLocalState) {
- PyModule module = new PyModule("__main__", dict);
- systemState.modules.__setitem__("__main__", module);
+ PyModule module = new PyModule("__main__", globals);
+ this.systemState.modules.__setitem__("__main__", module);
+ }
+
+ if (Options.Qnew) {
+ cflags.setFlag(CodeFlag.CO_FUTURE_DIVISION);
}
Py.importSiteIfSelected();
diff --git a/src/org/python/util/jython.java b/src/org/python/util/jython.java
--- a/src/org/python/util/jython.java
+++ b/src/org/python/util/jython.java
@@ -6,16 +6,18 @@
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
-import java.util.ArrayList;
-import java.util.Arrays;
+import java.io.PrintStream;
+import java.security.AccessControlException;
+import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.python.Version;
-import org.python.core.CodeFlag;
+import org.python.core.BytecodeLoader;
import org.python.core.CompileMode;
+import org.python.core.CompilerFlags;
import org.python.core.Options;
import org.python.core.Py;
import org.python.core.PyCode;
@@ -28,24 +30,30 @@
import org.python.core.PyStringMap;
import org.python.core.PySystemState;
import org.python.core.imp;
-import org.python.core.util.RelativeFile;
import org.python.modules._systemrestart;
-import org.python.modules.posix.PosixModule;
import org.python.modules.thread.thread;
public class jython {
+ /** Exit status: must have {@code OK.ordinal()==0} */
+ private enum Status {
+ OK, ERROR, NOT_RUN, SHOULD_RESTART, NO_FILE
+ }
+
// An instance of this class will provide the console (python.console) by default.
private static final String PYTHON_CONSOLE_CLASS = "org.python.util.JLineConsole";
private static final String COPYRIGHT =
"Type \"help\", \"copyright\", \"credits\" or \"license\" for more information.";
+ /** The message output when reporting command-line errors and when asked for help. */
static final String usageHeader =
"usage: jython [option] ... [-c cmd | -m mod | file | -] [arg] ...\n";
- private static final String usage = usageHeader
- + "Options and arguments:\n"
+ /** The message additional to {@link #usageHeader} output when asked for help. */
+ // @formatter:off
+ static final String usageBody =
+ "Options and arguments:\n"
// + "(and corresponding environment variables):\n"
+ "-B : don't write bytecode files on import\n"
// + "also PYTHONDONTWRITEBYTECODE=x\n" +
@@ -54,10 +62,10 @@
+ "-Dprop=v : Set the property `prop' to value `v'\n"
+ "-E : ignore environment variables (such as JYTHONPATH)\n"
+ "-h : print this help message and exit (also --help)\n"
- + "-i : inspect interactively after running script\n"
- // + ", (also PYTHONINSPECT=x)\n"
- + " and force prompts, even if stdin does not appear to be a terminal\n"
- + "-jar jar : program read from __run__.py in jar file\n"
+ + "-i : inspect interactively after running script; forces a prompt even"
+ + " if stdin does not appear to be a terminal; also PYTHONINSPECT=x\n"
+ + "-jar jar : program read from __run__.py in jar file (deprecated).\n"
+ + " Use PEP 338 support for jar file as argument (runs __main__.py).\n"
+ "-m mod : run library module as a script (terminates option list)\n"
// + "-O : optimize generated bytecode (a tad; also PYTHONOPTIMIZE=x)\n"
// + "-OO : remove doc-strings in addition to the -O optimizations\n"
@@ -80,22 +88,42 @@
+ "- : program read from stdin (default; interactive mode if a tty)\n"
+ "arg ... : arguments passed to program in sys.argv[1:]\n" + "\n"
+ "Other environment variables:\n" //
- + "JYTHONPATH: '" + File.pathSeparator
+ + "JYTHONSTARTUP: file executed on interactive startup (no default)\n"
+ + "JYTHONPATH : '" + File.pathSeparator
+ "'-separated list of directories prefixed to the default module\n"
+ " search path. The result is sys.path.\n"
+ "PYTHONIOENCODING: Encoding[:errors] used for stdin/stdout/stderr.";
+ // @formatter:on
- public static boolean shouldRestart;
+ /**
+ * Print a full usage message onto {@code System.out} or a brief usage message onto
+ * {@code System.err}.
+ *
+ * @param status if {@code == 0} full help version on {@code System.out}.
+ * @return the status given as the argument.
+ */
+ static Status usage(Status status) {
+ boolean fullHelp = (status == Status.OK);
+ PrintStream f = fullHelp ? System.out : System.err;
+ f.printf(usageHeader);
+ if (fullHelp) {
+ f.printf(usageBody);
+ } else {
+ f.println("Try 'jython -h' for more information.");
+ }
+ return status;
+ }
/**
* Runs a JAR file, by executing the code found in the file __run__.py, which should be in the
- * root of the JAR archive. Note that the __name__ is set to the base name of the JAR file and
- * not to "__main__" (for historic reasons). This method do NOT handle exceptions. the caller
- * SHOULD handle any (Py)Exceptions thrown by the code.
+ * root of the JAR archive. Note that {@code __name__} is set to the base name of the JAR file
+ * and not to "__main__" (for historical reasons). This method does <b>not</b> handle
+ * exceptions. the caller <b>should</b> handle any {@code (Py)Exceptions} thrown by the code.
*
* @param filename The path to the filename to run.
+ * @return {@code 0} on normal termination (otherwise throws {@code PyException}).
*/
- public static void runJar(String filename) {
+ public static int runJar(String filename) {
// TBD: this is kind of gross because a local called `zipfile' just magically
// shows up in the module's globals. Either `zipfile' should be called
// `__zipfile__' or (preferably, IMO), __run__.py should be imported and a main()
@@ -103,378 +131,494 @@
// argument.
//
// Probably have to keep this code around for backwards compatibility (?)
- try {
- ZipFile zip = new ZipFile(filename);
+ try (ZipFile zip = new ZipFile(filename)) {
ZipEntry runit = zip.getEntry("__run__.py");
if (runit == null) {
- throw Py.ValueError("jar file missing '__run__.py'");
+ throw Py.ValueError("can't find '__run__.py' in '" + filename + "'");
+ }
+
+ /*
+ * Stripping the stuff before the last File.separator fixes Bug #931129 by keeping
+ * illegal characters out of the generated proxy class name. Mostly.
+ */
+ int beginIndex = filename.lastIndexOf(File.separator);
+ if (beginIndex >= 0) {
+ filename = filename.substring(beginIndex + 1);
}
PyStringMap locals = Py.newStringMap();
-
- // Stripping the stuff before the last File.separator fixes Bug #931129 by
- // keeping illegal characters out of the generated proxy class name
- int beginIndex;
- if ((beginIndex = filename.lastIndexOf(File.separator)) != -1) {
- filename = filename.substring(beginIndex + 1);
- }
-
- locals.__setitem__("__name__", new PyString(filename));
+ locals.__setitem__("__name__", Py.fileSystemEncode(filename));
locals.__setitem__("zipfile", Py.java2py(zip));
- InputStream file = zip.getInputStream(runit);
- PyCode code;
- try {
- code = Py.compile(file, "__run__", CompileMode.exec);
- } finally {
- file.close();
- }
+ InputStream file = zip.getInputStream(runit); // closed when zip is closed
+
+ PyCode code = Py.compile(file, "__run__", CompileMode.exec);
Py.runCode(code, locals, locals);
+
} catch (IOException e) {
throw Py.IOError(e);
}
+
+ return Status.OK.ordinal();
}
public static void main(String[] args) {
+ Status status;
do {
- shouldRestart = false;
- run(args);
- } while (shouldRestart);
+ status = run(args);
+ } while (status == Status.SHOULD_RESTART);
+ System.exit(status.ordinal());
}
- private static List<String> warnOptionsFromEnv() {
- ArrayList<String> opts = new ArrayList<String>();
-
- try {
- String envVar = System.getenv("PYTHONWARNINGS");
- if (envVar == null) {
- return opts;
- }
-
- for (String opt : envVar.split(",")) {
- opt = opt.trim();
- if (opt.length() == 0) {
- continue;
- }
- opts.add(opt);
+ /**
+ * Append options from the environment variable {@code PYTHONWARNINGS}, respecting the -E
+ * option.
+ *
+ * @param opts the list to which the options are appended.
+ */
+ private static void addWarnOptionsFromEnv(PyList opts) {
+ String envVar = getenv("PYTHONWARNINGS", "");
+ for (String opt : envVar.split(",")) {
+ opt = opt.trim();
+ if (opt.length() > 0) {
+ opts.add(Py.fileSystemEncode(opt));
}
- } catch (SecurityException e) {
- // Continue
- }
-
- return opts;
- }
-
- private static List<String> validWarnActions = Arrays.asList("error", "ignore", "always",
- "default", "module", "once");
-
- private static void addWarnings(List<String> from, PyList to) {
- outerLoop : for (String opt : from) {
- String action = opt.split(":")[0];
- for (String validWarnAction : validWarnActions) {
- if (validWarnAction.startsWith(action)) {
- if (opt.contains(":")) {
- to.append(Py.newString(validWarnAction + opt.substring(opt.indexOf(":"))));
- } else {
- to.append(Py.newString(validWarnAction));
- }
- continue outerLoop;
- }
- }
- System.err.println(String.format("Invalid -W option ignored: invalid action: '%s'",
- action));
}
}
- private static void runModule(InteractiveConsole interp, String moduleName) {
- runModule(interp, moduleName, false);
+ /**
+ * Attempt to run a module as the {@code __main__} module, via a call to
+ * {@code runpy._run_module_as_main}. Exceptions raised by the imported module, including
+ * {@code SystemExit}, if not handled by {@code runpy} itself, will propagate out of this
+ * method. Note that if {@code runpy} cannot import the module it calls {@code sys.exit} with a
+ * message, which will raise {@code SystemExit} from this method.
+ *
+ * @param moduleName to run
+ * @param set_argv0 replace {@code sys.argv[0]} with the file name of the module source
+ * {@code runpy._run_module_as_main} option.
+ * @return {@code Status.OK} on normal termination.
+ */
+ private static Status runModule(String moduleName, boolean set_argv0) {
+ // PEP 338 - Execute module as a script
+ PyObject runpy = imp.importName("runpy", true);
+ PyObject runmodule = runpy.__findattr__("_run_module_as_main");
+ // May raise SystemExit (with message)
+ runmodule.__call__(Py.fileSystemEncode(moduleName), Py.newBoolean(set_argv0));
+ return Status.OK;
+ }
+
+ /**
+ * Attempt to treat a file as a source of imports, import a module {@code __main__}, and run it.
+ * If the file is suitable (e.g. it's a directory or a ZIP archive) the method places the file
+ * first on {@code sys.path}, so that {@code __main__} and its packaged dependencies may be
+ * found in it. This permits a zip file containing Python source to be run when given as a first
+ * argument on the command line. It may be that the file is not of a type that can be imported,
+ * in which case the return indicates this.
+ *
+ * @param archive to run (FS encoded name)
+ * @return {@code Status.OK} or {@code Status.NOT_RUN} (if the file was not an archive).
+ */
+ private static Status runMainFromImporter(PyString archive) {
+ PyObject importer = imp.getImporter(archive);
+ if (!(importer instanceof PyNullImporter)) {
+ // filename is usable as an import source, so put it in sys.path[0] and import __main__
+ Py.getSystemState().path.insert(0, archive);
+ return runModule("__main__", false);
+ }
+ return Status.NOT_RUN;
}
- private static void runModule(InteractiveConsole interp, String moduleName, boolean set_argv0) {
- // PEP 338 - Execute module as a script
- try {
- PyObject runpy = imp.importName("runpy", true);
- PyObject runmodule = runpy.__findattr__("_run_module_as_main");
- runmodule.__call__(Py.fileSystemEncode(moduleName), Py.newBoolean(set_argv0));
- } catch (Throwable t) {
- Py.printException(t);
- interp.cleanup();
- System.exit(-1);
+ /**
+ * Execute the stream {@code fp} as a file, in the given interpreter, as {@code __main__}. The
+ * file name provided must correspond to the stream. In particular, the file name extension
+ * "$py.class" will cause the stream to be interpreted as compiled code. For streams that are
+ * not really files, this name may be a conventional one like {@code "<stdin>"}, however this
+ * method will not treat a console stream as interactive.
+ *
+ * @param fp Python source code
+ * @param filename appears FS-encoded in variable {@code __file__} and in error messages
+ * @param interp to do the work
+ * @return {@code Status.OK} on normal termination.
+ */
+ // This is roughly equivalent to CPython PyRun_SimpleFileExFlags
+ private static Status runSimpleFile(InputStream fp, String filename, PythonInterpreter interp) {
+
+ // Reflect the current name in the module's __file__, compare PyRun_SimpleFileExFlags.
+ final String __file__ = "__file__";
+ PyObject globals = interp.globals;
+ PyObject previousFilename = globals.__finditem__(__file__);
+ if (previousFilename == null) {
+ globals.__setitem__(__file__,
+ // Note that __file__ is widely expected to be encoded bytes
+ Py.fileSystemEncode(filename));
+ }
+
+ // Allow for already-compiled target, but for us it's a $py.class not a .pyc.
+ if (filename.endsWith("$py.class")) {
+ // Jython compiled file.
+ String name = filename.substring(0, filename.length() - 6); // = - ".class"
+ try {
+ byte[] codeBytes = imp.readCode(filename, fp, false, imp.NO_MTIME);
+ File file = new File(filename);
+ PyCode code = BytecodeLoader.makeCode(name, codeBytes, file.getParent());
+ interp.exec(code);
+ } catch (IOException e) {
+ throw Py.IOError(e);
+ }
+ } else {
+ // Assume Python source file: run in the interpreter
+ interp.execfile(fp, filename);
+ }
+
+ // Delete __file__ variable, previously non-existent. Compare PyRun_SimpleFileExFlags.
+ if (previousFilename == null) {
+ globals.__delitem__(__file__);
+ }
+ return Status.OK;
+ }
+
+ /**
+ * Execute the stream {@code fp} in the given interpreter. If {@code fp} refers to a stream
+ * associated with an interactive device (console or terminal input), execute Python source
+ * statements from the stream in the interpreter as {@code __main__}. Otherwise, the file name
+ * provided must correspond to the stream, as in
+ * {@link #runSimpleFile(InputStream, String, PythonInterpreter)}.
+ *
+ * @param fp Python source code
+ * @param filename the name of the file or {@code null} meaning "???"
+ * @param interp to do the work
+ * @return {@code Status.OK} on normal termination.
+ */
+ // This is roughly equivalent to CPython PyRun_AnyFileExFlags
+ private static Status runStream(InputStream fp, String filename, InteractiveConsole interp) {
+ // Following CPython PyRun_AnyFileExFlags here, blindly, concerning null name.
+ filename = filename != null ? filename : "???";
+ // Run the contents in the interpreter
+ if (isInteractive(fp, filename)) {
+ // __file__ not defined
+ interp.interact(null, new PyFile(fp));
+ } else {
+ // __file__ will be defined
+ runSimpleFile(fp, filename, interp);
+ }
+ return Status.OK;
+ }
+
+ /**
+ * Attempt to open the named file and execute it in the interpreter as {@code __main__}, as in
+ * {@link #runStream(InputStream, String, InteractiveConsole)}. This may raise a Python
+ * exception, including {@code SystemExit}. If the file cannot be opened, or using it throws a
+ * Java {@code IOException} that is not converted to a {@code PyException} (i.e. not within the
+ * executing code), it is reported via {@link #printError(String, Object...)}, and reflected in
+ * the return status. If the file can be opened, its parent directory will be inserted at
+ * {@code sys.argv[0]}.
+ *
+ * @param filename the name of the file or {@code null} meaning "???"
+ * @param interp to do the work
+ * @return {@code Status.OK} on normal termination, {@code Status.NO_FILE} if the file cannot be
+ * read, or {@code Status.ERROR} on other {@code IOException}s.
+ */
+ private static Status runFile(String filename, InteractiveConsole interp) {
+ File file = new File(filename);
+ try (InputStream is = new FileInputStream(file)) {
+ String parent = file.getAbsoluteFile().getParent();
+ interp.getSystemState().path.insert(0, Py.fileSystemEncode(parent));
+ // May raise exceptions, (including SystemExit)
+ return runStream(is, filename, interp);
+ } catch (FileNotFoundException fnfe) {
+ // Couldn't open it. No point in going interactive, even if -i given.
+ printError("can't open file '%s': %s", filename, fnfe);
+ return Status.NO_FILE;
+ } catch (IOException ioe) {
+ // This may happen on the automatically-generated close()
+ printError("error closing '%s': %s", filename, ioe);
+ return Status.ERROR;
}
}
- private static boolean runMainFromImporter(InteractiveConsole interp, String filename) {
- // Support http://bugs.python.org/issue1739468 - Allow interpreter to execute a zip file or directory
- PyString argv0 = Py.fileSystemEncode(filename);
- PyObject importer = imp.getImporter(argv0);
- if (!(importer instanceof PyNullImporter)) {
- /* argv0 is usable as an import source, so
- put it in sys.path[0] and import __main__ */
- Py.getSystemState().path.insert(0, argv0);
- runModule(interp, "__main__", true);
- return true;
+ /**
+ * Attempt to execute the file named in the registry entry {@code python.startup}, which may
+ * also have been set via the environment variable {@code JYTHONSTARTUP}. This may raise a
+ * Python exception, including {@code SystemExit} that propagates to the caller. If the file
+ * cannot be opened, or using it throws a Java {@code IOException} that is not converted to a
+ * {@code PyException} (i.e. not within the executing code), it is reported via
+ * {@link #printError(String, Object...)}.
+ *
+ * @param interp to do the work
+ */
+ private static void runStartupFile(InteractiveConsole interp) {
+ String filename = PySystemState.registry.getProperty("python.startup", null);
+ if (filename != null) {
+ try (InputStream fp = new FileInputStream(filename)) {
+ // May raise exceptions, (including SystemExit)
+ // RunStreamOrThrow(fp, filename, interp);
+ runSimpleFile(fp, filename, interp);
+ } catch (FileNotFoundException fnfe) {
+ // Couldn't open it. No point in going interactive, even if -i given.
+ printError("Could not open startup file '%s'", filename);
+ } catch (IOException ioe) {
+ // This may happen on the automatically-generated close()
+ printError("error closing '%s': %s", filename, ioe);
+ }
}
- return false;
}
- public static void run(String[] args) {
-
+ /**
+ * Main Jython program, following the structure and logic of CPython {@code main.c} to produce
+ * the same behaviour. The argument to the method is the argument list supplied after the class
+ * name in the {@code java} command. Arguments up to the executable script name are options for
+ * Jython; arguments after the executable script are supplied in {@code sys.argv}. "Executable
+ * script" here means a Python source file name, a module name (following the {@code -m}
+ * option), a literal command (following the {@code -c} option), or a JAR file name (following
+ * the {@code -jar} option). As a special case of the file name, "-" is allowed, meaning take
+ * the script from standard input.
+ * <p>
+ * The main difference for the caller stems from a difference between C and Java: in C, the
+ * argument list {@code (argv)} begins with the program name, while in Java all elements of
+ * {@code (args)} are arguments to the program.
+ *
+ * @param args arguments to the program.
+ * @return status indicating outcome.
+ */
+ public static Status run(String[] args) {
// Parse the command line options
- CommandLineOptions opts = new CommandLineOptions();
- if (!opts.parse(args)) {
- if (opts.version) {
- System.err.println("Jython " + Version.PY_VERSION);
- System.exit(0);
- }
- if (opts.help) {
- System.err.println(usage);
- } else if (!opts.runCommand && !opts.runModule) {
- System.err.print(usageHeader);
- System.err.println("Try `jython -h' for more information.");
- }
-
- int exitcode = opts.help ? 0 : -1;
- System.exit(exitcode);
+ CommandLineOptions opts = CommandLineOptions.parse(args);
+ switch (opts.action) {
+ case VERSION:
+ System.err.printf("Jython %s\n", Version.PY_VERSION);
+ return Status.OK;
+ case HELP:
+ return usage(Status.OK);
+ case ERROR:
+ System.err.println(opts.message);
+ return usage(Status.ERROR);
+ case RUN:
+ // Let's run some Python! ...
}
// Get system properties (or empty set if we're prevented from accessing them)
- Properties preProperties = PySystemState.getBaseProperties();
+ Properties preProperties = getSystemProperties();
+ addDefaultsFromEnvironment(preProperties);
- // Read environment variable PYTHONIOENCODING into properties (registry)
- String pythonIoEncoding = getenv("PYTHONIOENCODING");
- if (pythonIoEncoding != null) {
- String[] spec = splitString(pythonIoEncoding, ':', 2);
- // Note that if encoding or errors is blank (=null), the registry value wins.
- addDefault(preProperties, PySystemState.PYTHON_IO_ENCODING, spec[0]);
- addDefault(preProperties, PySystemState.PYTHON_IO_ERRORS, spec[1]);
+ // Treat the apparent filename "-" as no filename
+ boolean haveDash = "-".equals(opts.filename);
+ if (haveDash) {
+ opts.filename = null;
}
- // If/when we interact with standard input, will we use a line-editing console?
- boolean stdinIsInteractive = Py.isInteractive();
+ // Sense whether the console is interactive, or we have been told to consider it so.
+ boolean stdinIsInteractive = isInteractive(System.in, null);
- // Decide if System.in is interactive
- if (!opts.fixInteractive || opts.interactive) {
- // The options suggest System.in is interactive: but only if isatty() agrees
- opts.interactive = stdinIsInteractive;
- if (opts.interactive) {
+ // Shorthand
+ boolean haveScript = opts.command != null || opts.filename != null || opts.module != null;
+
+ if (Options.inspect || !haveScript) {
+ // We'll be going interactive eventually. condition an interactive console.
+ if (haveConsole()) {
// Set the default console type if nothing else has
addDefault(preProperties, "python.console", PYTHON_CONSOLE_CLASS);
}
}
- // Setup the basic python system state from these options
- PySystemState.initialize(preProperties, opts.properties, opts.argv);
- PySystemState systemState = Py.getSystemState();
+ /*
+ * Set up the executable-wide state from the options, environment and registry, and create
+ * the first instance of a sys module. We try to leave to this initialisation the things
+ * necessary to an embedded interpreter, and to do in the present method only that which
+ * belongs only to command line application.
+ *
+ * (Jython partitions system and interpreter state differently from modern CPython, which is
+ * able explicitly to create a PyInterpreterState first, after which sys and the thread
+ * state are created to hang from it.)
+ */
+ // The Jython type system will spring into existence here. This may take a while.
+ PySystemState.initialize(preProperties, opts.properties);
+ // Now we can use PyObjects safely.
+ PySystemState sys = Py.getSystemState();
- PyList warnoptions = new PyList();
- addWarnings(opts.warnoptions, warnoptions);
- if (!Options.ignore_environment) {
- addWarnings(warnOptionsFromEnv(), warnoptions);
- }
- systemState.setWarnoptions(warnoptions);
-
- // Make sure warnings module is loaded if there are warning options
- // Not sure this is needed, but test_warnings.py expects this
- if (warnoptions.size() > 0) {
+ /*
+ * Jython initialisation does not load the "warnings" module. Rather we defer it to here,
+ * where we may safely prepare sys.warnoptions from the -W arguments and the contents of
+ * PYTHONWARNINGS (compare PEP 432).
+ */
+ addFSEncoded(opts.warnoptions, sys.warnoptions);
+ addWarnOptionsFromEnv(sys.warnoptions);
+ if (!sys.warnoptions.isEmpty()) {
+ // The warnings module validates (and may complain about) the warning options.
imp.load("warnings");
}
- // Now create an interpreter
- if (!opts.interactive) {
- // Not (really) interactive, so do not use console prompts
- systemState.ps1 = systemState.ps2 = Py.EmptyString;
- }
+ /*
+ * Create the interpreter that we will use as a name space in which to execute the script or
+ * interactive session. We run site.py as part of interpreter initialisation (as CPython).
+ */
InteractiveConsole interp = new InteractiveConsole();
- // Print banner and copyright information (or not)
- if (opts.interactive && opts.notice && !opts.runModule) {
+ if (Options.verbose > Py.MESSAGE || (!haveScript && stdinIsInteractive)) {
+ // Verbose or going interactive immediately: produce sign on messages.
System.err.println(InteractiveConsole.getDefaultBanner());
- }
-
- if (Py.importSiteIfSelected()) {
- if (opts.interactive && opts.notice && !opts.runModule) {
+ if (Options.importSite) {
System.err.println(COPYRIGHT);
}
}
- if (opts.division != null) {
- if ("old".equals(opts.division)) {
- Options.division_warning = 0;
- } else if ("warn".equals(opts.division)) {
- Options.division_warning = 1;
- } else if ("warnall".equals(opts.division)) {
- Options.division_warning = 2;
- } else if ("new".equals(opts.division)) {
- Options.Qnew = true;
- interp.cflags.setFlag(CodeFlag.CO_FUTURE_DIVISION);
+ /*
+ * We currently have sys.argv = PySystemState.defaultArgv = ['']. Python has a special use
+ * for sys.argv[0] according to the source of the script (-m, -c, etc.), but the rest of it
+ * comes from the unparsed part of the command line.
+ */
+ addFSEncoded(opts.argv, sys.argv);
+
+ /*
+ * At last, we are ready to execute something. This has two parts: execute the script or
+ * console and (if we didn't execute the console) optionally start an interactive console
+ * session. The sys.path needs to be prepared in a slightly different way for each case.
+ */
+ Status sts = Status.NOT_RUN;
+
+ try {
+ if (opts.command != null) {
+ // The script is an immediate command -c "..."
+ sys.argv.set(0, Py.newString("-c"));
+ sys.path.insert(0, Py.EmptyString);
+ interp.exec(opts.command);
+ sts = Status.OK;
+
+ } else if (opts.module != null) {
+ // The script is a module
+ sys.argv.set(0, Py.newString("-m"));
+ sts = runModule(opts.module, true);
+
+ } else if (opts.filename != null) {
+ // The script is designated by file (or directory) name.
+ PyString pyFileName = Py.fileSystemEncode(opts.filename);
+ sys.argv.set(0, pyFileName);
+
+ if (opts.jar) {
+ // The filename was given with the -jar option.
+ sys.path.insert(0, pyFileName);
+ runJar(opts.filename);
+ sts = Status.OK;
+
+ } else {
+ /*
+ * The filename was given as the leading argument after the options. Our first
+ * approach is to treat it as an archive (or directory) in which to find a
+ * __main__.py (as per PEP 338). The handler for this inserts the module at
+ * sys.path[0] if it runs. It may raise exceptions, but only SystemExit as runpy
+ * deals with the others.
+ */
+ sts = runMainFromImporter(pyFileName);
+
+ if (sts == Status.NOT_RUN) {
+ /*
+ * The filename was not a suitable source for import, so treat it as a file
+ * to execute. The handler for this inserts the parent of the file at
+ * sys.path[0].
+ */
+ sts = runFile(opts.filename, interp);
+ // If we really had no script, do not go interactive at the end.
+ haveScript = sts != Status.NO_FILE;
+ }
+ }
+
+ } else { // filename == null
+ // There is no script. (No argument or it was "-".)
+ if (haveDash) {
+ sys.argv.set(0, Py.newString('-'));
+ }
+ sys.path.insert(0, Py.EmptyString);
+
+ // Genuinely interactive, or just interpreting piped instructions?
+ if (stdinIsInteractive) {
+ // If genuinely interactive, SystemExit should mean exit the application.
+ Options.inspect = false;
+ // If genuinely interactive, run a start-up file if one is specified.
+ runStartupFile(interp);
+ }
+
+ // Run from console: exceptions other than SystemExit are handled in the REPL.
+ sts = runStream(System.in, "<stdin>", interp);
+ }
+
+ } catch (PyException pye) {
+ // Whatever the mode of execution an uncaught PyException lands here.
+ if (pye.match(_systemrestart.SystemRestart)) {
+ // Leave ourselves a note to restart.
+ sts = Status.SHOULD_RESTART;
+ } else {
+ // If pye was SystemExit *and* Options.inspect==false, this will exit the JVM:
+ Py.printException(pye);
+ // It was an exception other than SystemExit or Options.inspect==true.
+ sts = Status.ERROR;
}
}
- // was there a filename on the command line?
- if (opts.filename != null) {
- if (runMainFromImporter(interp, opts.filename)) {
- interp.cleanup();
- return;
- }
+ /*
+ * Check this environment variable at the end, to give programs the opportunity to set it
+ * from Python.
+ */
+ // XXX: Java does not let us set environment variables but we could have our own os.environ.
+ if (!Options.inspect) {
+ Options.inspect = getenv("PYTHONINSPECT") != null;
+ }
- String path;
+ if (Options.inspect && stdinIsInteractive && haveScript) {
+ /*
+ * The inspect flag is set (-i option) so we've been asked to end with an interactive
+ * session: the console is interactive, and we have just executed some kind of script
+ * (i.e. it wasn't already an interactive session).
+ */
try {
- path = new File(opts.filename).getCanonicalFile().getParent();
- } catch (IOException ioe) {
- path = new File(opts.filename).getAbsoluteFile().getParent();
- }
- if (path == null) {
- path = "";
- }
- Py.getSystemState().path.insert(0, Py.fileSystemEncode(path));
- if (opts.jar) {
- try {
- runJar(opts.filename);
- } catch (Throwable t) {
- Py.printException(t);
- System.exit(-1);
- }
- } else if (opts.filename.equals("-")) {
- try {
- interp.globals.__setitem__(new PyString("__file__"), new PyString("<stdin>"));
- interp.execfile(System.in, "<stdin>");
- } catch (Throwable t) {
- Py.printException(t);
- }
- } else {
- try {
- interp.globals.__setitem__(new PyString("__file__"),
- // Note that __file__ is widely expected to be encoded bytes
- Py.fileSystemEncode(opts.filename));
- FileInputStream file;
- try {
- file = new FileInputStream(new RelativeFile(opts.filename));
- } catch (FileNotFoundException e) {
- throw Py.IOError(e);
- }
- try {
- boolean isInteractive = false;
- try {
- isInteractive = PosixModule.getPOSIX().isatty(file.getFD());
- } catch (SecurityException ex) {}
- if (isInteractive) {
- opts.interactive = true;
- interp.interact(null, new PyFile(file));
- return;
- } else {
- interp.execfile(file, opts.filename);
- }
- } finally {
- file.close();
- }
- } catch (Throwable t) {
- if (t instanceof PyException
- && ((PyException)t).match(_systemrestart.SystemRestart)) {
- // Shutdown this instance...
- shouldRestart = true;
- shutdownInterpreter();
- interp.cleanup();
- // ..reset the state...
- Py.setSystemState(new PySystemState());
- // ...and start again
- return;
- } else {
- Py.printException(t);
- interp.cleanup();
- System.exit(-1);
- }
- }
- }
- } else {
- // if there was no file name on the command line, then "" is the first element
- // on sys.path. This is here because if there /was/ a filename on the c.l.,
- // and say the -i option was given, sys.path[0] will have gotten filled in
- // with the dir of the argument filename.
- Py.getSystemState().path.insert(0, Py.EmptyString);
-
- if (opts.command != null) {
- try {
- interp.exec(opts.command);
- } catch (Throwable t) {
- Py.printException(t);
- System.exit(1);
- }
- }
-
- if (opts.moduleName != null) {
- runModule(interp, opts.moduleName);
- interp.cleanup();
- return;
+ // Ensure that this time SystemExit means exit.
+ Options.inspect = false;
+ // Run from console: exceptions other than SystemExit are handled in the REPL.
+ sts = runStream(System.in, "<stdin>", interp);
+ } catch (PyException pye) {
+ // Exception from the execution of Python code.
+ Py.printException(pye); // SystemExit will exit the JVM here.
+ sts = Status.ERROR;
}
}
- if (opts.fixInteractive || (opts.filename == null && opts.command == null)) {
- // Go interactive with the console: the parser needs to know the encoding.
- String encoding = Py.getConsole().getEncoding();
- // Run the interpreter interactively
- try {
- interp.cflags.encoding = encoding;
- if (!opts.interactive) {
- // Don't print prompts. http://bugs.jython.org/issue2325
- interp._interact(null, null);
- }
- else {
- interp.interact(null, null);
- }
- } catch (Throwable t) {
- Py.printException(t);
- }
+ if (sts == Status.SHOULD_RESTART) {
+ // Shut down *all* threads and sockets
+ shutdownInterpreter();
+ // ..reset the state...
+ // XXX seems unnecessary given we will call PySystemState.initialize()
+ // Py.setSystemState(new PySystemState());
+ // ...and start again
}
+ // Shut down in a tidy way
interp.cleanup();
+ PySystemState.undoInitialize();
+
+ return sts;
}
/**
* Run any finalizations on the current interpreter in preparation for a SytemRestart.
*/
public static void shutdownInterpreter() {
- // Stop all the active threads and signal the SystemRestart
+ PySystemState sys = Py.getSystemState();
+ // Signal to threading.py to modify response to InterruptedException:
+ sys._systemRestart = true;
+ // Interrupt (stop?) all the active threads in the jython-threads group.
thread.interruptAllThreads();
- Py.getSystemState()._systemRestart = true;
- // Close all sockets -- not all of their operations are stopped by
- // Thread.interrupt (in particular pre-nio sockets)
+ // Close all sockets not already covered by Thread.interrupt (e.g. pre-nio sockets)
try {
- imp.load("socket").__findattr__("_closeActiveSockets").__call__();
+ PyObject socket = sys.modules.__finditem__("socket");
+ if (socket != null) {
+ // XXX Fossil code. Raises AttributeError as _closeActiveSockets has been deleted.
+ socket.__getattr__("_closeActiveSockets").__call__();
+ }
} catch (PyException pye) {
- // continue
+ // don't worry about errors: we're shutting down
}
}
/**
- * Return an array of trimmed strings by splitting the argument at each occurrence of a
- * separator character. (Helper for configuration variable processing.) Segments of zero length
- * after trimming emerge as <code>null</code>. If there are more than the specified number of
- * segments the last element of the array contains all of the source string after the
- * <code>(n-1)</code>th occurrence of <code>sep</code>.
- *
- * @param spec to split
- * @param sep character on which to split
- * @param n number of parts to split into
- * @return <code>n</code>-element array of strings (or <code>null</code>s)
- */
- private static String[] splitString(String spec, char sep, int n) {
- String[] list = new String[n];
- int p = 0, i = 0, L = spec.length();
- while (p < L) {
- int c = spec.indexOf(sep, p);
- if (c < 0 || i >= n - 1) {
- // No more seps, or no more space: i.th piece is the rest of spec.
- c = L;
- }
- String s = spec.substring(p, c).trim();
- list[i++] = (s.length() > 0) ? s : null;
- p = c + 1;
- }
- return list;
- }
-
- /**
* If the key is not currently present and the passed value is not <code>null</code>, sets the
* <code>key</code> to the <code>value</code> in the given <code>Properties</code> object. Thus,
* it provides a default value for a subsequent <code>getProperty()</code>.
@@ -495,244 +639,414 @@
}
/**
- * Get the value of an environment variable, if we are allowed to and it exists; otherwise
- * return <code>null</code>. We are allowed to access the environment variable if the -E flag
- * was not given and the application has permission to read environment variables. The -E flag
- * is reflected in {@link Options#ignore_environment}, and will be set automatically if it turns
- * out we do not have permission.
+ * Provides default registry entries from particular supported environment variables, obtained
+ * by calls to {@link #getenv(String)}. If a corresponding entry already exists in the
+ * properties passed, it takes precedence.
*
- * @param varname name to access in the environment
- * @return the value or <code>null</code>.
+ * @param registry to be (possibly) updated
*/
- private static String getenv(String varname) {
+ private static void addDefaultsFromEnvironment(Properties registry) {
+ // Runs at the start of each (wholly) interactive session.
+ addDefault(registry, "python.startup", getenv("JYTHONSTARTUP"));
+ // Go interactive after script. (PYTHONINSPECT because Python scripts may set it.)
+ addDefault(registry, "python.inspect", getenv("PYTHONINSPECT"));
+
+ // Read environment variable PYTHONIOENCODING into properties (registry)
+ String pythonIoEncoding = getenv("PYTHONIOENCODING");
+ if (pythonIoEncoding != null) {
+ String[] spec = pythonIoEncoding.split(":", 2);
+ // Note that if encoding or errors is blank (=null), the registry value wins.
+ addDefault(registry, PySystemState.PYTHON_IO_ENCODING, spec[0]);
+ if (spec.length > 1) {
+ addDefault(registry, PySystemState.PYTHON_IO_ERRORS, spec[1]);
+ }
+ }
+ }
+
+ /** The same as {@code getenv(name, null)} */
+ private static String getenv(String name) {
+ return getenv(name, null);
+ }
+
+ /**
+ * Get the value of an environment variable, if we are allowed to and it is defined; otherwise,
+ * return the chosen default value. An empty string value is treated as undefined. We are
+ * allowed to access the environment variable if if the JVM security environment permits and if
+ * {@link Options#ignore_environment} is {@code false}, which it is by default. It may be set by
+ * the -E flag given to the launcher, or by program action, or once it turns out we do not have
+ * permission (saving further work).
+ *
+ * @param name to access in the environment.
+ * @param defaultValue to return if {@code name} is not defined or "" or access is forbidden.
+ * @return the corresponding value or <code>defaultValue</code>.
+ */
+ private static String getenv(String name, String defaultValue) {
if (!Options.ignore_environment) {
try {
- return System.getenv(varname);
+ String value = System.getenv(name);
+ return (value != null && value.length() > 0) ? value : defaultValue;
} catch (SecurityException e) {
// We're not allowed to access them after all
Options.ignore_environment = true;
}
}
- return null;
+ return defaultValue;
+ }
+
+ private static void optionNotSupported(char option) {
+ printError("Option -%c is not supported", option);
+ }
+
+ /**
+ * Print {@code "jython: <formatted args>"} on {@code System.err} as one line.
+ *
+ * @param format suitable to use in {@code String.format(format, args)}
+ * @param args zero or more args
+ */
+ private static void printError(String format, Object... args) {
+ System.err.println(String.format("jython: " + format, args));
}
-}
-
-class CommandLineOptions {
-
- public String filename;
- public boolean jar, notice;
- public boolean runCommand, runModule;
- /** True unless a file, module, jar, or command argument awaits execution. */
- public boolean interactive = true;
- /** Eventually go interactive: reflects the -i ("inspect") flag. */
- public boolean fixInteractive = false;
- public boolean help, version;
- public String[] argv;
- public Properties properties;
- public String command;
- public List<String> warnoptions = Generic.list();
- public String encoding;
- public String division;
- public String moduleName;
-
- public CommandLineOptions() {
- filename = null;
- jar = false;
- notice = true;
- runModule = false;
- properties = new Properties();
- help = version = false;
+ /**
+ * Check whether an input stream is interactive. This emulates CPython
+ * {@code Py_FdIsInteractive} within the constraints of pure Java.
+ *
+ * The input stream is considered ``interactive'' if either
+ * <ol type="a">
+ * <li>it is {@code System.in} and {@code System.console()} is not {@code null}, or</li>
+ * <li>the {@code -i} flag was given ({@link Options#interactive}={@code true}), and the
+ * filename associated with it is {@code null} or {@code"<stdin>"} or {@code "???"}.</li>
+ * </ol>
+ *
+ * @param fp stream (tested only for {@code System.in})
+ * @param filename
+ * @return true iff thought to be interactive
+ */
+ private static boolean isInteractive(InputStream fp, String filename) {
+ if (fp == System.in && haveConsole()) {
+ return true;
+ } else if (!Options.interactive) {
+ return false;
+ } else {
+ return filename == null || filename.equals("<stdin>") || filename.equals("???");
+ }
}
- public void setProperty(String key, String value) {
- properties.put(key, value);
+ /** Return {@code true} iff the console is accessible through System.console(). */
+ private static boolean haveConsole() {
try {
- System.setProperty(key, value);
- } catch (SecurityException e) {
- // continue
+ return System.console() != null;
+ } catch (SecurityException se) {
+ return false;
+ }
+ }
+
+ /**
+ * Get the System properties if we are allowed to. Configuration values set via
+ * {@code -Dprop=value} to the java command will be found here. If a security manager prevents
+ * access, we will return a new (empty) object instead.
+ *
+ * @return {@code System} properties or a new {@code Properties} object
+ */
+ private static Properties getSystemProperties() {
+ try {
+ return System.getProperties();
+ } catch (AccessControlException ace) {
+ return new Properties();
+ }
+ }
+
+ /**
+ * Append strings to a PyList as {@code bytes/str} objects. These might come from the command
+ * line, or any source with the possibility of non-ascii values.
+ *
+ * @param source of {@code String}s
+ * @param destination list
+ */
+ private static void addFSEncoded(Iterable<String> source, PyList destination) {
+ for (String s : source) {
+ destination.add(Py.fileSystemEncode(s));
}
}
- private boolean argumentExpected(String arg) {
- System.err.println("Argument expected for the " + arg + " option");
- return false;
- }
+ /**
+ * Class providing a parser for Jython command line options. Many of the allowable options set
+ * values directly in the static {@link Options} as the parser runs, while others set values in
+ * (an instance) of this class.
+ */
+ static class CommandLineOptions {
+
+ enum Action {
+ RUN, ERROR, HELP, VERSION
+ };
- public boolean parse(String[] args) {
- int index = 0;
+ Action action = Action.RUN;
+ String message = "";
+
+ String command;
+ String filename;
+ String module;
+
+ boolean help = false;
+ boolean version = false;
+ boolean jar = false;
+
+ Properties properties = new Properties();
- while (index < args.length && args[index].startsWith("-")) {
- String arg = args[index];
- if (arg.equals("-h") || arg.equals("-?") || arg.equals("--help")) {
- help = true;
- return false;
- } else if (arg.equals("-V") || arg.equals("--version")) {
- version = true;
- return false;
- } else if (arg.equals("-")) {
- if (!fixInteractive) {
- interactive = false;
- }
- filename = "-";
- } else if (arg.equals("-i")) {
- fixInteractive = true;
- interactive = true;
- } else if (arg.equals("-jar")) {
- jar = true;
- if (!fixInteractive) {
- interactive = false;
- }
- } else if (arg.equals("-u")) {
- Options.unbuffered = true;
- } else if (arg.equals("-v")) {
- Options.verbose++;
- } else if (arg.equals("-vv")) {
- Options.verbose += 2;
- } else if (arg.equals("-vvv")) {
- Options.verbose += 3;
- } else if (arg.equals("-s")) {
- Options.no_user_site = true;
- } else if (arg.equals("-S")) {
- Options.importSite = false;
- } else if (arg.equals("-B")) {
- Options.dont_write_bytecode = true;
- } else if (arg.startsWith("-c")) {
- runCommand = true;
- if (arg.length() > 2) {
- command = arg.substring(2);
- } else if ((index + 1) < args.length) {
- command = args[++index];
- } else {
- return argumentExpected(arg);
- }
- if (!fixInteractive) {
- interactive = false;
- }
- index++;
- break;
- } else if (arg.startsWith("-W")) {
- if (arg.length() > 2) {
- warnoptions.add(arg.substring(2));
- } else if ((index + 1) < args.length) {
- warnoptions.add(args[++index]);
- } else {
- return argumentExpected(arg);
- }
- } else if (arg.equals("-E")) {
- // -E (ignore environment variables)
- Options.ignore_environment = true;
- } else if (arg.startsWith("-D")) {
- String key = null;
- String value = null;
- int equals = arg.indexOf("=");
- if (equals == -1) {
- String arg2 = args[++index];
- key = arg.substring(2, arg.length());
- value = arg2;
- } else {
- key = arg.substring(2, equals);
- value = arg.substring(equals + 1, arg.length());
- }
- setProperty(key, value);
- } else if (arg.startsWith("-Q")) {
- if (arg.length() > 2) {
- division = arg.substring(2);
- } else {
- division = args[++index];
- }
- } else if (arg.startsWith("-m")) {
- runModule = true;
- if (arg.length() > 2) {
- moduleName = arg.substring(2);
- } else if ((index + 1) < args.length) {
- moduleName = args[++index];
- } else {
- return argumentExpected(arg);
- }
- if (!fixInteractive) {
- interactive = false;
- }
+ List<String> argv = new LinkedList<String>();
+ List<String> warnoptions = new LinkedList<String>();
+ CompilerFlags cf = new CompilerFlags();
+
+ /** Valid single character options. ':' means expect an argument following. */
+ // XJD are extra to CPython. X and J are sanctioned while D is potentially a clash.
+ static final String PROGRAM_OPTS = "3bBc:dEhim:OQ:RsStuUvVW:x?" + "XJD";
- index++;
- int n = args.length - index + 1;
- argv = new String[n];
- argv[0] = moduleName;
- for (int i = 1; index < args.length; i++, index++) {
- argv[i] = args[index];
- }
- return true;
- } else if (arg.startsWith("-3")) {
- Options.py3k_warning = true;
- } else {
- String opt = args[index];
- if (opt.startsWith("--")) {
- opt = opt.substring(2);
- } else if (opt.startsWith("-")) {
- opt = opt.substring(1);
- }
- System.err.println("Unknown option: " + opt);
- return false;
- }
- index += 1;
- }
- notice = interactive;
- if (filename == null && index < args.length && command == null) {
- filename = args[index++];
- if (!fixInteractive) {
- interactive = false;
- }
- notice = false;
- }
- if (command != null) {
- notice = false;
+ /** Valid long-name options. */
+ static final char JAR_OPTION = '\u2615';
+ static final OptionScanner.LongSpec[] PROGRAM_LONG_OPTS = {
+ // new OptionScanner.LongSpec("-", OptionScanner.DONE),
+ new OptionScanner.LongSpec("--", OptionScanner.DONE),
+ new OptionScanner.LongSpec("--help", 'h'),
+ new OptionScanner.LongSpec("--version", 'v'),
+ new OptionScanner.LongSpec("-jar", JAR_OPTION, true), // Yes, just one dash.
+ };
+
+ /**
+ * Parse the arguments into the static {@link Options} and a returned instance of this
+ * class.
+ *
+ * @param args from program invocation.
+ * @return
+ */
+ static CommandLineOptions parse(String args[]) {
+ CommandLineOptions opts = new CommandLineOptions();
+ opts._parse(args);
+ return opts;
}
- int n = args.length - index + 1;
-
- /* Exceptionally we allow -J-Dcpython_cmd=... also postpone the filename.
- * E.g. the Linux launcher allows this already on launcher level for all
- * -J flags, while the Windows launcher does not.
- *
- * Todo: Resolve this discrepancy!
- *
- * This is required to use cpython_cmd property in context of pip, e.g.
- * pip install --global-option="-J-Dcpython_cmd=python" <package>
- * For details about the cpython_cmd property, look into
- * org.python.compiler.Module.loadPyBytecode source.
- */
- int cpython_cmd_pos = -1;
- for (int i = index; i < args.length; i++) {
- if (args[i].startsWith("-J-Dcpython_cmd=")) {
- cpython_cmd_pos = i;
- System.setProperty("cpython_cmd", args[i].substring(16));
- n--;
- break;
+ /** Parser implementation. Do not call this twice on the same instance. */
+ private void _parse(String args[]) {
+ // Create a scanner with the right tables for Python/Jython
+ OptionScanner scanner = new OptionScanner(args, PROGRAM_OPTS, PROGRAM_LONG_OPTS);
+ _parse(scanner, args);
+ if (action == Action.RUN) {
+ // Squirrel away the unprocessed arguments
+ while (scanner.countRemainingArguments() > 0) {
+ argv.add(scanner.getWholeArgument());
+ }
}
}
- argv = new String[n];
- if (filename != null) {
- argv[0] = filename;
- } else if (command != null) {
- argv[0] = "-c";
- } else {
- argv[0] = "";
- }
+ /**
+ * Parse options into object state, until we encounter the first argument. This is a helper
+ * to {@link #_parse(String[])}.
+ */
+ private void _parse(OptionScanner scanner, String args[]) {
+ char c;
+ /*
+ * The default action is RUN, taken when we all the options have been processed, and
+ * either we have run out of arguments (we'll start an interactive session) or
+ * encountered a non-option argument, which ought to name the file to execute.
+ * Executable options (like -m and -c) cause a return with action==RUN from their case.
+ * Any errors, and some special options like --help and -V, return set some other action
+ * than RUN, ending the loop.
+ */
+ while (action == Action.RUN && (c = scanner.getOption()) != OptionScanner.DONE) {
+
+ switch (c) {
+ /*
+ * The first 4 cases all terminate the options in with a RUN action, meaning
+ * that this option defines the executable script and the arguments following
+ * will be passed to the script.
+ */
+ case 'c':
+ /*
+ * -c is the last option; following arguments that look like options are
+ * left for the command to interpret.
+ */
+ command = scanner.getOptionArgument() + "\n";
+ return;
+
+ case 'm':
+ /*
+ * -m is the last option; following arguments that look like options are
+ * left for the module to interpret.
+ */
+ module = scanner.getOptionArgument();
+ return;
+
+ case JAR_OPTION:
+ /*
+ * -jar is the last option; following arguments that look like options are
+ * left for __run__.py to interpret.
+ */
+ jar = true;
+ filename = scanner.getOptionArgument();
+ return;
+
+ case OptionScanner.ARGUMENT:
+ /*
+ * This should be a file name (or "-", meaning stdin); following arguments
+ * that look like options are left for the code it contains to interpret.
+ */
+ filename = scanner.getWholeArgument();
+ return;
+
+ // Options that don't terminate option processing (mostly).
+
+ case 'b':
+ case 'd':
+ optionNotSupported(c);
+ break;
+
+ case '3':
+ Options.py3k_warning = true;
+ if (Options.division_warning == 0) {
+ Options.division_warning = 1;
+ }
+ break;
+
+ case 'Q':
+ switch (scanner.getOptionArgument()) {
+ case "old":
+ Options.division_warning = 0;
+ break;
+ case "warn":
+ Options.division_warning = 1;
+ break;
+ case "warnall":
+ Options.division_warning = 2;
+ break;
+ case "new":
+ Options.Qnew = true;
+ default:
+ error("-Q option should be `-Qold', "
+ + "`-Qwarn', `-Qwarnall', or `-Qnew' only");
+ }
+ break;
+
+ case 'i':
+ Options.inspect = Options.interactive = true;
+ break;
- if (cpython_cmd_pos == -1) {
- for (int i = 1; i < n; i++, index++) {
- argv[i] = args[index];
- }
- } else {
- for (int i = 1; i < n; i++, index++) {
- if (index == cpython_cmd_pos) {
- index++;
+ case 'O':
+ Options.optimize++;
+ break;
+
+ case 'B':
+ Options.dont_write_bytecode = true;
+ break;
+
+ case 's':
+ Options.no_user_site = true;
+ break;
+
+ case 'S':
+ Options.no_site = true;
+ Options.importSite = false;
+ break;
+
+ case 'E':
+ Options.ignore_environment = true;
+ break;
+
+ case 't':
+ optionNotSupported(c);
+ // Py_TabcheckFlag++;
+ break;
+
+ case 'u':
+ Options.unbuffered = true;
+ break;
+
+ case 'v':
+ Options.verbose++;
+ break;
+
+ case 'x':
+ optionNotSupported(c);
+ // skipfirstline = true;
+ break;
+
+ // case 'X': reserved for implementation-specific arguments
+
+ case 'U':
+ optionNotSupported(c);
+ // Py_UnicodeFlag++;
+ break;
+
+ case 'W':
+ warnoptions.add(scanner.getOptionArgument());
+ break;
+
+ case 'R':
+ optionNotSupported(c);
+ break;
+
+ case 'D':
+ // Definitions made on the command line: -Dprop=v
+ try {
+ optionD(scanner);
+ } catch (SecurityException e) {
+ // Prevented by security policy.
+ }
+ break;
+
+ // Options that terminate option processing with something other than RUN.
+
+ case 'h':
+ case '?':
+ action = Action.HELP;
+ break;
+
+ case 'V':
+ action = Action.VERSION;
+ break;
+
+ case 'J':
+ /*
+ * This shouldn't happen because the launcher should have recognised this
+ * and converted it to an option or argument to the java command. If it
+ * shows up here, maybe it was supplied outside the loader or the context
+ * has confused the launcher.
+ */
+ error("-J is only valid when using the Jython launcher. "
+ + "In a complex command, put the -J options early.");
+ break;
+
+ case OptionScanner.ERROR:
+ error(scanner.getMessage());
+ break;
+
+ default:
+ // Acceptable to the scanner, but missing from the case statement?
+ error("parser did not recognise option -%c \\u%04x", c, c);
+ break;
}
- argv[i] = args[index];
}
}
- return true;
+ /**
+ * Helper for option {@code -Dprop=v}. This is potentially a clash with Python: work around
+ * for luncher misplacement of -J-D...?
+ */
+ private void optionD(OptionScanner scanner) throws SecurityException {
+ String[] kv = scanner.getWholeArgument().split("=", 2);
+ String prop = kv[0].trim();
+ if (kv.length > 1) {
+ properties.put(prop, kv[1]);
+ } else {
+ properties.put(prop, "");
+ }
+ }
+
+ /**
+ * Set the error message as {@code String.format(message, args)} and set the action to
+ * {@link Action#ERROR}.
+ */
+ private void error(String message, Object... args) {
+ this.message = args.length == 0 ? message : String.format(message, args);
+ action = Action.ERROR;
+ }
}
}
--
Repository URL: https://hg.python.org/jython
More information about the Jython-checkins
mailing list