Zbyszek Szmek <zbys...@in.waw.pl> added the comment:
Well, right now it's just one function. Functionality which you propose
could of course be useful, but let's leave if for another day, another
issue.
See also http://bugs.python.org/issue13609#msg149627 and #444582 and
#12442 -- shutil is growing in different directions, and terminal size
can be one of them.
> Antoine Pitrou added the comment:
> "except Exception" clauses in the tests are too broad.
Fixed.
> Otherwise, looks fine.
> STINNER Victor added the comment:
> - shutil.get_terminal_size(): I would prefer columns and lines
> variable names instead of "ccc" and "rrr"
Changed (this was a leftover from the version when there was no named
tuple).
> - I would prefer os.get_terminal_size() instead of
> os.query_terminal_size(), I didn't know other function using the verb
> "query" in Python
Changed. I used os.g_t_s and shutil.g_t_s in docs so it should be clear
which is which.
> - To keep os simple, os.query_terminal_size() can return a simple,
> whereas shutil.get_terminal_size() returns a namedtuple
We would have two very similar functions returning something different.
Also, the amount of code saved will be negligible, because the named
tuple is mostly docs. I prefer to keep it in os to keep both functions
similar.
> - test_os.py: use @unittest.skipUnless() on TermsizeTests to check if
> os.query_terminal_size() is available
> - To keep os simple, os.query_terminal_size() can return a simple,
> whereas shutil.get_terminal_size() returns a namedtuple
> AttributeError should be catched here, not NameError. Or better, check
> if os has a query_terminal_size() function.
Fixed. I changed the tests a bit, e.g. to take into account that stty
queries stdin, not stdout.
> Hum, you may document the default value: (80, 24).
Done.
shutil.get_terminal_size() is tied to COLUMNS and ROWS and thus makes
most sense for stdout. But I guess that an optional parameter could be
added:
shutil.get_terminal_size(fd=None, fallback=(80, 24))
where fd could be either an integer, or an object with .fileno().
I don't know.
> - "Right now the values are not cached, but this might change." why
> would it be cached? this sentence can just be removed
Done. In theory it could be cached with watching for SIGWINCH, but I'll
just remove the comment for now.
Thanks for the review and comments!
Patch version nine attached: termsize.diff.8
----------
Added file: http://bugs.python.org/file24379/termsize.diff.8
_______________________________________
Python tracker <rep...@bugs.python.org>
<http://bugs.python.org/issue13609>
_______________________________________
diff --git a/Doc/library/os.rst b/Doc/library/os.rst
--- a/Doc/library/os.rst
+++ b/Doc/library/os.rst
@@ -1408,6 +1408,44 @@
.. versionadded:: 3.3
+.. _terminal-size:
+
+Querying the size of the output terminal
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. versionadded:: 3.3
+
+.. function:: get_terminal_size(fd=STDOUT_FILENO)
+
+ Return the size of the terminal window as ``(columns, lines)``,
+ tuple of type :class:`terminal_size`.
+
+ The optional argument ``fd`` (default ``STDOUT_FILENO``, or standard
+ output) specifies which file descriptor should be queried.
+
+ If the file descriptor is not connected to a terminal, an :exc:`OSError`
+ is thrown.
+
+ This function is only present if an implementation is available for
+ this system (currently POSIX and Windows).
+
+ :func:`shutil.get_terminal_size` is the high-level function which
+ should normally be used, ``os.get_terminal_size`` is the low-level
+ implementation.
+
+.. class:: terminal_size(tuple)
+
+ A tuple of ``(columns, lines)`` for holding terminal window size.
+
+ .. attribute:: columns
+
+ Width of the terminal window in characters.
+
+ .. attribute:: lines
+
+ Height of the terminal window in characters.
+
+
.. _os-file-dir:
Files and Directories
diff --git a/Doc/library/shutil.rst b/Doc/library/shutil.rst
--- a/Doc/library/shutil.rst
+++ b/Doc/library/shutil.rst
@@ -450,3 +450,33 @@
-rw-r--r-- tarek/staff 37192 2010-02-06 18:23:10 ./known_hosts
+Querying the size of the output terminal
+----------------------------------------
+
+.. versionadded:: 3.3
+
+.. function:: get_terminal_size(fallback=(columns, lines))
+
+ Get the size of the terminal window.
+
+ For each of the two dimensions, the environment variable, ``COLUMNS``
+ and ``LINES`` respectively, is checked. If the variable is defined and
+ the value is a positive integer, it is used.
+
+ When ``COLUMNS`` or ``LINES`` is not defined, which is the common case,
+ the terminal connected to :data:`sys.__stdout__` is queried
+ by invoking :func:`os.get_terminal_size`.
+
+ If the terminal size cannot be successfully queried, either because
+ the system doesn't support querying, or because we are not
+ connected to a terminal, the value given in ``fallback`` parameter
+ is used. ``fallback`` defaults to ``(80, 24)`` which is the default
+ size used by many terminal emulators.
+
+ The value returned is a named tuple of type :class:`os.terminal_size`.
+
+ See also: The Single UNIX Specification, Version 2,
+ `Other Environment Variables`_.
+
+.. _`Other Environment Variables`:
+ http://pubs.opengroup.org/onlinepubs/7908799/xbd/envvar.html#tag_002_003
diff --git a/Doc/whatsnew/3.3.rst b/Doc/whatsnew/3.3.rst
--- a/Doc/whatsnew/3.3.rst
+++ b/Doc/whatsnew/3.3.rst
@@ -469,6 +469,12 @@
(Patch submitted by Giampaolo Rodolà in :issue:`10784`.)
+* XXX The :mod:`shutil` module has a new function
+ :func:`~shutil.get_terminal_size` to check the size of output terminal
+ window.
+
+ (Patch submitted by Zbigniew JÄdrzejewski-Szmek in :issue:`13609`.)
+
* "at" functions (:issue:`4761`):
* :func:`~os.faccessat`
diff --git a/Lib/shutil.py b/Lib/shutil.py
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -872,3 +872,46 @@
raise LookupError("no such group: {!r}".format(group))
os.chown(path, _user, _group)
+
+def get_terminal_size(fallback=(80, 24)):
+ """Get the size of the terminal window.
+
+ For each of the two dimensions, the environment variable, COLUMNS
+ and LINES respectively, is checked. If the variable is defined and
+ the value is a positive integer, it is used.
+
+ When COLUMNS or LINES is not defined, which is the common case,
+ the terminal connected to sys.__stdout__ is queried
+ by invoking os.get_terminal_size.
+
+ If the terminal size cannot be successfully queried, either because
+ the system doesn't support querying, or because we are not
+ connected to a terminal, the value given in fallback parameter
+ is used. Fallback defaults to (80, 24) which is the default
+ size used by many terminal emulators.
+
+ The value returned is a named tuple of type os.terminal_size.
+ """
+ # columns, lines are the working values
+ try:
+ columns = int(os.environ['COLUMNS'])
+ except (KeyError, ValueError):
+ columns = 0
+
+ try:
+ lines = int(os.environ['LINES'])
+ except (KeyError, ValueError):
+ lines = 0
+
+ # only query if necessary
+ if columns <= 0 or lines <= 0:
+ try:
+ size = os.get_terminal_size(sys.__stdout__.fileno())
+ except (NameError, OSError):
+ size = os.terminal_size(fallback)
+ if columns <= 0:
+ columns = size.columns
+ if lines <= 0:
+ lines = size.lines
+
+ return os.terminal_size((columns, lines))
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -1676,6 +1676,43 @@
os.symlink, filename, filename)
+@unittest.skipUnless(hasattr(os, 'get_terminal_size'), "requires
os.get_terminal_size")
+class TermsizeTests(unittest.TestCase):
+ def test_does_not_crash(self):
+ """Check if get_terminal_size() returns a meaningful value.
+
+ There's no easy portable way to actually check the size of the
+ terminal, so let's check if it returns something sensible instead.
+ """
+ try:
+ size = os.get_terminal_size()
+ except OSError as e:
+ if e.errno == errno.EINVAL or sys.platform == "win32":
+ # Under win32 a generic OSError can be thrown if the
+ # handle cannot be retrieved
+ self.skipTest("failed to query terminal size")
+ raise
+
+ self.assertGreater(size.columns, 0)
+ self.assertGreater(size.lines, 0)
+
+ def test_stty_match(self):
+ """Check if stty returns the same results
+
+ stty actually tests stdin, so get_terminal_size is invoked on
+ stdin explicitly. If stty succeeded, then get_terminal_size()
+ should work too.
+ """
+ try:
+ size = subprocess.check_output(['stty', 'size']).decode().split()
+ except (FileNotFoundError, subprocess.CalledProcessError):
+ self.skipTest("stty invocation failed")
+ expected = (int(size[1]), int(size[0])) # reversed order
+
+ actual = os.get_terminal_size(sys.__stdin__.fileno())
+ self.assertEqual(expected, actual)
+
+
@support.reap_threads
def test_main():
support.run_unittest(
@@ -1701,6 +1738,7 @@
ProgramPriorityTests,
ExtendedAttributeTests,
Win32DeprecatedBytesAPI,
+ TermsizeTests,
)
if __name__ == "__main__":
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -8,6 +8,7 @@
import os
import os.path
import functools
+import subprocess
from test import support
from test.support import TESTFN
from os.path import splitdrive
@@ -1223,10 +1224,55 @@
finally:
os.rmdir(dst_dir)
+class TermsizeTests(unittest.TestCase):
+ def test_does_not_crash(self):
+ """Check if get_terminal_size() returns a meaningful value.
+
+ There's no easy portable way to actually check the size of the
+ terminal, so let's check if it returns something sensible instead.
+ """
+ size = shutil.get_terminal_size()
+ self.assertGreater(size.columns, 0)
+ self.assertGreater(size.lines, 0)
+
+ def test_os_environ_first(self):
+ "Check if environment variables have precedence"
+
+ with support.EnvironmentVarGuard() as env:
+ env['COLUMNS'] = '777'
+ size = shutil.get_terminal_size()
+ self.assertEqual(size.columns, 777)
+
+ with support.EnvironmentVarGuard() as env:
+ env['LINES'] = '888'
+ size = shutil.get_terminal_size()
+ self.assertEqual(size.lines, 888)
+
+ @unittest.skipUnless(os.isatty(sys.__stdout__.fileno()), "not on tty")
+ def test_stty_match(self):
+ """Check if stty returns the same results ignoring env
+
+ This test will fail if stdin and stdout are connected to
+ different terminals with different sizes. Nevertheless, such
+ situations should be pretty rare.
+ """
+ try:
+ size = subprocess.check_output(['stty', 'size']).decode().split()
+ except (FileNotFoundError, subprocess.CalledProcessError):
+ self.skipTest("stty invocation failed")
+ expected = (int(size[1]), int(size[0])) # reversed order
+
+ with support.EnvironmentVarGuard() as env:
+ del env['LINES']
+ del env['COLUMNS']
+ actual = shutil.get_terminal_size()
+
+ self.assertEqual(expected, actual)
def test_main():
- support.run_unittest(TestShutil, TestMove, TestCopyFile)
+ support.run_unittest(TestShutil, TestMove, TestCopyFile,
+ TermsizeTests)
if __name__ == '__main__':
test_main()
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -7289,6 +7289,9 @@
- Windows locale mapping updated to Vista.
+- Issue #13609: shutil.get_terminal_size() and os.get_terminal_size()
+ functions are added to check the size of the terminal window.
+
Tools/Demos
-----------
diff --git a/Modules/posixmodule.c b/Modules/posixmodule.c
--- a/Modules/posixmodule.c
+++ b/Modules/posixmodule.c
@@ -125,6 +125,18 @@
#include <dlfcn.h>
#endif
+#if defined(MS_WINDOWS)
+# define TERMSIZE_USE_CONIO
+#elif defined(HAVE_SYS_IOCTL_H)
+# include <sys/ioctl.h>
+# if defined(HAVE_TERMIOS_H)
+# include <termios.h>
+# endif
+# if defined(TIOCGWINSZ)
+# define TERMSIZE_USE_IOCTL
+# endif
+#endif /* MS_WINDOWS */
+
/* Various compilers have only certain posix functions */
/* XXX Gosh I wish these were all moved into pyconfig.h */
#if defined(PYCC_VACPP) && defined(PYOS_OS2)
@@ -10496,6 +10508,114 @@
#endif /* USE_XATTRS */
+
+/* Terminal size querying */
+
+static PyTypeObject TerminalSizeType;
+
+PyDoc_STRVAR(TerminalSize_docstring,
+ "A tuple of (columns, lines) for holding terminal window size");
+
+static PyStructSequence_Field TerminalSize_fields[] = {
+ {"columns", "width of the terminal window in characters"},
+ {"lines", "height of the terminal window in characters"},
+ {NULL, NULL}
+};
+
+static PyStructSequence_Desc TerminalSize_desc = {
+ "os.terminal_size",
+ TerminalSize_docstring,
+ TerminalSize_fields,
+ 2,
+};
+
+#if defined(TERMSIZE_USE_CONIO) || defined(TERMSIZE_USE_IOCTL)
+PyDoc_STRVAR(termsize__doc__,
+ "Return the size of the terminal window as (columns, lines).\n" \
+ "\n" \
+ "The optional argument fd (default standard output) specifies\n" \
+ "which file descriptor should be queried.\n" \
+ "\n" \
+ "If the file descriptor is not connected to a terminal, an OSError\n" \
+ "is thrown.\n" \
+ "\n" \
+ "This function will only be defined if an implementation is\n" \
+ "available for this system.\n" \
+ "\n" \
+ "shutil.get_terminal_size is the high-level function which should \n" \
+ "normally be used, os.get_terminal_size is the low-level implementation.");
+
+static PyObject*
+get_terminal_size(PyObject *self, PyObject *args)
+{
+ int columns, lines;
+ PyObject *termsize;
+
+ int fd = fileno(stdout);
+ /* Under some conditions stdout may not be connected and
+ * fileno(stdout) may point to an invalid file descriptor. For example
+ * GUI apps don't have valid standard streams by default.
+ *
+ * If this happens, and the optional fd argument is not present,
+ * the ioctl below will fail returning EBADF. This is what we want.
+ */
+
+ if (!PyArg_ParseTuple(args, "|i", &fd))
+ return NULL;
+
+#ifdef TERMSIZE_USE_IOCTL
+ {
+ struct winsize w;
+ if (ioctl(fd, TIOCGWINSZ, &w))
+ return PyErr_SetFromErrno(PyExc_OSError);
+ columns = w.ws_col;
+ lines = w.ws_row;
+ }
+#endif /* TERMSIZE_USE_IOCTL */
+
+#ifdef TERMSIZE_USE_CONIO
+ {
+ DWORD nhandle;
+ HANDLE handle;
+ CONSOLE_SCREEN_BUFFER_INFO csbi;
+ switch (fd) {
+ case 0: nhandle = STD_INPUT_HANDLE;
+ break;
+ case 1: nhandle = STD_OUTPUT_HANDLE;
+ break;
+ case 2: nhandle = STD_ERROR_HANDLE;
+ break;
+ default:
+ return PyErr_Format(PyExc_ValueError, "bad file descriptor");
+ }
+ handle = GetStdHandle(nhandle);
+ if (handle == NULL)
+ return PyErr_Format(PyExc_OSError, "handle cannot be retrieved");
+ if (handle == INVALID_HANDLE_VALUE)
+ return PyErr_SetFromWindowsErr(0);
+
+ if (!GetConsoleScreenBufferInfo(handle, &csbi))
+ return PyErr_SetFromWindowsErr(0);
+
+ columns = csbi.srWindow.Right - csbi.srWindow.Left + 1;
+ lines = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
+ }
+#endif /* TERMSIZE_USE_CONIO */
+
+ termsize = PyStructSequence_New(&TerminalSizeType);
+ if (termsize == NULL)
+ return NULL;
+ PyStructSequence_SET_ITEM(termsize, 0, PyLong_FromLong(columns));
+ PyStructSequence_SET_ITEM(termsize, 1, PyLong_FromLong(lines));
+ if (PyErr_Occurred()) {
+ Py_DECREF(termsize);
+ return NULL;
+ }
+ return termsize;
+}
+#endif /* defined(TERMSIZE_USE_CONIO) || defined(TERMSIZE_USE_IOCTL) */
+
+
static PyMethodDef posix_methods[] = {
{"access", posix_access, METH_VARARGS, posix_access__doc__},
#ifdef HAVE_TTYNAME
@@ -10963,6 +11083,9 @@
{"llistxattr", posix_llistxattr, METH_VARARGS, posix_llistxattr__doc__},
{"flistxattr", posix_flistxattr, METH_VARARGS, posix_flistxattr__doc__},
#endif
+#if defined(TERMSIZE_USE_CONIO) || defined(TERMSIZE_USE_IOCTL)
+ {"get_terminal_size", get_terminal_size, METH_VARARGS, termsize__doc__},
+#endif
{NULL, NULL} /* Sentinel */
};
@@ -11557,6 +11680,10 @@
PyStructSequence_InitType(&SchedParamType, &sched_param_desc);
SchedParamType.tp_new = sched_param_new;
#endif
+
+ /* initialize TerminalSize_info */
+ PyStructSequence_InitType(&TerminalSizeType, &TerminalSize_desc);
+ Py_INCREF(&TerminalSizeType);
}
#if defined(HAVE_WAITID) && !defined(__APPLE__)
Py_INCREF((PyObject*) &WaitidResultType);
@@ -11611,6 +11738,9 @@
#endif /* __APPLE__ */
+
+ PyModule_AddObject(m, "terminal_size", (PyObject*) &TerminalSizeType);
+
return m;
}
diff --git a/configure b/configure
--- a/configure
+++ b/configure
@@ -6147,7 +6147,7 @@
sys/audioio.h sys/xattr.h sys/bsdtty.h sys/event.h sys/file.h sys/loadavg.h \
sys/lock.h sys/mkdev.h sys/modem.h \
sys/param.h sys/select.h sys/sendfile.h sys/socket.h sys/statvfs.h \
-sys/stat.h sys/termio.h sys/time.h \
+sys/stat.h sys/termio.h sys/time.h sys/ioctl.h \
sys/times.h sys/types.h sys/uio.h sys/un.h sys/utsname.h sys/wait.h pty.h \
libutil.h sys/resource.h netpacket/packet.h sysexits.h bluetooth.h \
bluetooth/bluetooth.h linux/tipc.h spawn.h util.h
diff --git a/configure.in b/configure.in
--- a/configure.in
+++ b/configure.in
@@ -1337,7 +1337,7 @@
sys/audioio.h sys/xattr.h sys/bsdtty.h sys/event.h sys/file.h sys/loadavg.h \
sys/lock.h sys/mkdev.h sys/modem.h \
sys/param.h sys/select.h sys/sendfile.h sys/socket.h sys/statvfs.h \
-sys/stat.h sys/termio.h sys/time.h \
+sys/stat.h sys/termio.h sys/time.h sys/ioctl.h \
sys/times.h sys/types.h sys/uio.h sys/un.h sys/utsname.h sys/wait.h pty.h \
libutil.h sys/resource.h netpacket/packet.h sysexits.h bluetooth.h \
bluetooth/bluetooth.h linux/tipc.h spawn.h util.h)
diff --git a/pyconfig.h.in b/pyconfig.h.in
--- a/pyconfig.h.in
+++ b/pyconfig.h.in
@@ -905,6 +905,9 @@
/* Define to 1 if you have the <sys/file.h> header file. */
#undef HAVE_SYS_FILE_H
+/* Define to 1 if you have the <sys/ioctl.h> header file. */
+#undef HAVE_SYS_IOCTL_H
+
/* Define to 1 if you have the <sys/loadavg.h> header file. */
#undef HAVE_SYS_LOADAVG_H
_______________________________________________
Python-bugs-list mailing list
Unsubscribe:
http://mail.python.org/mailman/options/python-bugs-list/archive%40mail-archive.com