Skip to content

Commit

Permalink
Issue #13609: Add two functions to query the terminal size:
Browse files Browse the repository at this point in the history
os.get_terminal_size (low level) and shutil.get_terminal_size (high level).
Patch by Zbigniew Jędrzejewski-Szmek.
  • Loading branch information
pitrou committed Feb 8, 2012
1 parent 4195b5c commit bcf2b59
Show file tree
Hide file tree
Showing 10 changed files with 339 additions and 5 deletions.
37 changes: 37 additions & 0 deletions Doc/library/os.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1411,6 +1411,43 @@ or `the MSDN <http://msdn.microsoft.com/en-us/library/z0kc8e3z.aspx>`_ on Window
.. versionadded:: 3.3


.. _terminal-size:

Querying the size of a 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.

:func:`shutil.get_terminal_size` is the high-level function which
should normally be used, ``os.get_terminal_size`` is the low-level
implementation.

Availability: Unix, Windows.

.. 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
Expand Down
33 changes: 33 additions & 0 deletions Doc/library/shutil.rst
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,36 @@ The resulting archive contains::
-rw------- tarek/staff 1675 2008-06-09 13:26:54 ./id_rsa
-rw-r--r-- tarek/staff 397 2008-06-09 13:26:54 ./id_rsa.pub
-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

43 changes: 43 additions & 0 deletions Lib/shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -878,3 +878,46 @@ def chown(path, user=None, group=None):
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))
38 changes: 38 additions & 0 deletions Lib/test/test_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,43 @@ def test_symlink(self):
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(
Expand All @@ -1866,6 +1903,7 @@ def test_main():
ProgramPriorityTests,
ExtendedAttributeTests,
Win32DeprecatedBytesAPI,
TermsizeTests,
)

if __name__ == "__main__":
Expand Down
48 changes: 47 additions & 1 deletion Lib/test/test_shutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import os.path
import errno
import functools
import subprocess
from test import support
from test.support import TESTFN
from os.path import splitdrive
Expand Down Expand Up @@ -1267,10 +1268,55 @@ def test_move_dir_caseinsensitive(self):
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()
4 changes: 4 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -466,6 +466,10 @@ Core and Builtins
Library
-------

- Issue #13609: Add two functions to query the terminal size:
os.get_terminal_size (low level) and shutil.get_terminal_size (high level).
Patch by Zbigniew Jędrzejewski-Szmek.

- Issue #13845: On Windows, time.time() now uses GetSystemTimeAsFileTime()
instead of ftime() to have a resolution of 100 ns instead of 1 ms (the clock
accuracy is between 0.5 ms and 15 ms).
Expand Down
Loading

0 comments on commit bcf2b59

Please sign in to comment.