summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
Diffstat (limited to 'Lib')
-rw-r--r--Lib/shutil.py43
-rw-r--r--Lib/test/test_os.py38
-rw-r--r--Lib/test/test_shutil.py48
3 files changed, 128 insertions, 1 deletions
diff --git a/Lib/shutil.py b/Lib/shutil.py
index db80faf..6664599 100644
--- a/Lib/shutil.py
+++ b/Lib/shutil.py
@@ -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))
diff --git a/Lib/test/test_os.py b/Lib/test/test_os.py
index 4d27c2b..8dd745a 100644
--- a/Lib/test/test_os.py
+++ b/Lib/test/test_os.py
@@ -1840,6 +1840,43 @@ class Win32DeprecatedBytesAPI(unittest.TestCase):
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(
@@ -1866,6 +1903,7 @@ def test_main():
ProgramPriorityTests,
ExtendedAttributeTests,
Win32DeprecatedBytesAPI,
+ TermsizeTests,
)
if __name__ == "__main__":
diff --git a/Lib/test/test_shutil.py b/Lib/test/test_shutil.py
index c72bac2..4d0ef29 100644
--- a/Lib/test/test_shutil.py
+++ b/Lib/test/test_shutil.py
@@ -9,6 +9,7 @@ import os
import os.path
import errno
import functools
+import subprocess
from test import support
from test.support import TESTFN
from os.path import splitdrive
@@ -1267,10 +1268,55 @@ class TestCopyFile(unittest.TestCase):
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()