From 196284a2fe8aec1e0f362cc0100dccbc79ace07b Mon Sep 17 00:00:00 2001 From: Gerard Lanois Date: Mon, 9 Mar 2026 00:52:19 -0700 Subject: [PATCH 1/4] #72 Work in progress - aligning wc.py with Gnu wc. --- python/ppt/wc.py | 165 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 121 insertions(+), 44 deletions(-) diff --git a/python/ppt/wc.py b/python/ppt/wc.py index ff0f371..b1a82f0 100644 --- a/python/ppt/wc.py +++ b/python/ppt/wc.py @@ -29,31 +29,43 @@ import re import sys + +def countrequested(count): + num_requested = 0 + for (k, v) in list(count['requests'].items()): + if v['requested']: + num_requested += 1 + return num_requested + + def wordcounttext(options, text, filepath): count = { - 'chars' : 0, - 'lines' : 0, - 'words' : 0 } - count['chars'] = len(text) - count['lines'] = text.count('\n') - count['words'] = len(re.findall(r"[\w']+|[.,!?;]", text)) - - if options.bytes or options.chars: - print('%7d %s' % (count['chars'], filepath)) - elif options.lines: - print('%7d %s' % (count['lines'], filepath)) - elif options.words: - print('%7d %s' % (count['words'], filepath)) - else: - print('%7d %7d %7d %s' % (count['lines'], count['words'], count['chars'], filepath)) + 'requests' : { + 'bytes' : { + 'count' : os.stat(filepath).st_size, + 'requested' : options.bytes }, + 'chars' : { + 'count' : len(text), + 'requested' : options.chars }, + 'lines' : { + 'count' : text.count('\n'), + 'requested' : options.lines }, + 'words' : { + 'count' : len(re.findall(r"[\w']+|[.,!?;]", text)), + 'requested' : options.words} }, + 'filepath' : filepath } + + if countrequested(count) == 0: + # Nothing explicitly requested. Therefore, request lines, + # words, and bytes. + count['requests']['lines']['requested'] = True + count['requests']['words']['requested'] = True + count['requests']['bytes']['requested'] = True + return count - + def wordcountfile(options, filepath): - count = { - 'chars' : 0, - 'lines' : 0, - 'words' : 0 } try: fin = open(filepath, 'r') text = fin.read() @@ -62,31 +74,81 @@ def wordcountfile(options, filepath): print('Error reading file %s.' % (filepath)) raise else: - count = wordcounttext(options, text, filepath) - return count - + return wordcounttext(options, text, filepath) + +def measurewidth(count): + width = 0 + # Find the width of the widest requested count. + for v in count['requests'].values(): + if v['requested'] and len(str(v['count'])) > width: + width = len(str(v['count'])) + return width + + def wordcountfilenames(options): total = { - 'chars' : 0, - 'lines' : 0, - 'words' : 0 } - - for path in options.filenames: - count = wordcountfile(options, path) - total = {k: total[k] + v for (k, v) in list(count.items())} + 'bytes' : { + 'count' : 0, + 'requested' : False }, + 'chars' : { + 'count' : 0, + 'requested' : False }, + 'lines' : { + 'count' : 0, + 'requested' : False }, + 'words' : { + 'count' : 0, + 'requested' : False } } + + # Find the width of the widest requested count. + widths = [] + counts = [] + for filepath in options.filenames: + count = wordcountfile(options, filepath) + width = measurewidth(count) + counts.append(count) + widths.append(width) + for k in count['requests'].keys(): + if count['requests'][k]['requested']: + total[k]['count'] = total[k]['count'] + count['requests'][k]['count'] + total[k]['requested'] = count['requests'][k]['requested'] + + return counts, widths, total + - if len(options.filenames) > 1: - if options.bytes or options.chars: - print('%7d %s' % (total['chars'], 'total')) - elif options.lines: - print('%7d %s' % (total['lines'], 'total')) +def printcountsbyfile(count, width): + + # Have to do some work to duplicate the way wc spaces the numbers. + if countrequested(count) == 1: + # 1. When only one command line switch count requested, there are no leading spaces. + if options.lines: + print(f"{count['requests']['lines']['count']} {count['filepath']}") + elif options.bytes: + print(f"{count['requests']['bytes']['count']} {count['filepath']}") + elif options.chars: + print(f"{count['requests']['chars']['count']} {count['filepath']}") elif options.words: - print('%7d %s' % (total['words'], 'total')) - else: - print('%7d %7d %7d %s' % (total['lines'], total['words'], total['chars'], 'total')) - - + print(f"{count['requests']['words']['count']} {count['filepath']}") + else: + # 2. When there are + # a) no requested values (which means print all counts), or + # b) more than one requested value, + # + # wc uses the width of the widest requested number + # (plus one space between as a separator), and always + # prints them in this order: lines, words, characters, bytes. + + # Assemble them in the specific order: lines, words, characters, bytes. + counts = [] + for c in ('lines', 'words', 'chars', 'bytes'): + if count['requests'][c]['requested']: + counts.append(f"{count['requests'][c]['count']:>{width}}") + + # Print it out. + print(f"{' '.join(counts)} {count['filepath']}") + + def main(options): if len(options.filenames) == 0: # No filenames given on the command line. @@ -95,14 +157,29 @@ def main(options): filenames = sys.stdin.read() # Split on NUL and throw away last one due to last NUL terminator. options.filenames = filenames.split('\x00')[:-1] - wordcountfilenames(options) + counts, widths, total = wordcountfilenames(options) else: # Process data directly from stdin. text = sys.stdin.read() - wordcounttext(options, text, '') + count = wordcounttext(options, text, '') + elif len(options.filenames) == 1: + # Just one file on the command line. + count = wordcountfile(options, options.filenames[0]) + width = measurewidth(count) + printcountsbyfile(count, width) else: - wordcountfilenames(options) - + # Multiple files. + counts, widths, total = wordcountfilenames(options) + for count in counts: + printcountsbyfile(count, max(widths)) + + totals = [] + for c in ('lines', 'words', 'chars', 'bytes'): + if total[c]['requested']: + totals.append(f"{total[c]['count']:>{max(widths)}}") + + print(f"{' '.join(totals)} total") + def get_parser(): parser = argparse.ArgumentParser() From 94514fed0efe192199ad8909bc0efb81d1c2b60f Mon Sep 17 00:00:00 2001 From: Gerard Lanois Date: Mon, 9 Mar 2026 02:13:29 -0700 Subject: [PATCH 2/4] #72 Work in progress - debugged and tested reading from stdin. --- python/ppt/tests/testwc.py | 20 +++++++++++--------- python/ppt/wc.py | 32 ++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/python/ppt/tests/testwc.py b/python/ppt/tests/testwc.py index 6c05eee..15bea1b 100644 --- a/python/ppt/tests/testwc.py +++ b/python/ppt/tests/testwc.py @@ -86,8 +86,8 @@ def test_wc_file(self): # Run all options on the file in turn. # Can't test the help options (-h, --help) because argparse exits after printing the help. fixture = [ - { 'sw' : '-c', 'val' : 1456 }, - { 'sw' : '--bytes', 'val' : 1456 }, + { 'sw' : '-c', 'val' : 1464 }, + { 'sw' : '--bytes', 'val' : 1464 }, { 'sw' : '-m', 'val' : 1456 }, { 'sw' : '--chars', 'val' : 1456 }, { 'sw' : '-l', 'val' : 25 }, @@ -114,16 +114,16 @@ def test_wc_file2(self): warnings.simplefilter('ignore', ResourceWarning) # Capture stdout when running. - out = io.StringIO() - with redirect_stdout(out): + io_out = io.StringIO() + with redirect_stdout(io_out): parser = ppt.wc.get_parser() options = parser.parse_args([DATA_FILE]) ppt.wc.main(options) # Compare the results. - captured_stdout = out.getvalue() + captured_stdout = io_out.getvalue() vals_int = [int(v) for v in captured_stdout.split()[0:3]] - expected_vals = [25, 302, 1456] + expected_vals = [25, 302, 1464] self.assertEqual(vals_int, expected_vals) @@ -142,17 +142,19 @@ def test_wc_stdin(self): # TypeError: expected string or bytes-like object, got 'MagicMock' # # Therefore, this reads the whole file in (frown) and passes that. + with unittest.mock.patch('sys.stdin') as mock_stdin, open(DATA_FILE, 'r', encoding='utf-8') as data_file: data_file_contents = data_file.read() + mock_stdin.read.return_value = data_file_contents # Capture stdout when running. - out = io.StringIO() - with redirect_stdout(out): + io_out = io.StringIO() + with redirect_stdout(io_out): ppt.wc.main(options) # Compare the results. - captured_stdout = out.getvalue() + captured_stdout = io_out.getvalue() vals_int = [int(v) for v in captured_stdout.split()[0:3]] expected_vals = [25, 302, 1456] self.assertEqual(vals_int, expected_vals) diff --git a/python/ppt/wc.py b/python/ppt/wc.py index b1a82f0..0b83644 100644 --- a/python/ppt/wc.py +++ b/python/ppt/wc.py @@ -39,10 +39,22 @@ def countrequested(count): def wordcounttext(options, text, filepath): + # For stdin (filepath == ''), just use the length of + # the incoming text. + bytes_count = len(text) + + # For actual files, use the size on disk. + if os.path.isfile(filepath): + bytes_count = os.stat(filepath).st_size + + # Note that the size on disk will be greater than the + # the length of the text when there are unicode characters. + # (eg, em dash instead of plain ASCII minus '-') + count = { 'requests' : { 'bytes' : { - 'count' : os.stat(filepath).st_size, + 'count' : bytes_count, 'requested' : options.bytes }, 'chars' : { 'count' : len(text), @@ -66,14 +78,8 @@ def wordcounttext(options, text, filepath): def wordcountfile(options, filepath): - try: - fin = open(filepath, 'r') + with open(filepath, 'r', encoding='utf-8') as fin: text = fin.read() - fin.close() - except IOError: - print('Error reading file %s.' % (filepath)) - raise - else: return wordcounttext(options, text, filepath) @@ -122,13 +128,13 @@ def printcountsbyfile(count, width): # Have to do some work to duplicate the way wc spaces the numbers. if countrequested(count) == 1: # 1. When only one command line switch count requested, there are no leading spaces. - if options.lines: + if count['requests']['lines']['requested']: print(f"{count['requests']['lines']['count']} {count['filepath']}") - elif options.bytes: + elif count['requests']['bytes']['requested']: print(f"{count['requests']['bytes']['count']} {count['filepath']}") - elif options.chars: + elif count['requests']['chars']['requested']: print(f"{count['requests']['chars']['count']} {count['filepath']}") - elif options.words: + elif count['requests']['words']['requested']: print(f"{count['requests']['words']['count']} {count['filepath']}") else: # 2. When there are @@ -162,6 +168,8 @@ def main(options): # Process data directly from stdin. text = sys.stdin.read() count = wordcounttext(options, text, '') + width = measurewidth(count) + printcountsbyfile(count, width) elif len(options.filenames) == 1: # Just one file on the command line. count = wordcountfile(options, options.filenames[0]) From 230bd7466623a1812969ea1224cfba8e19ca427f Mon Sep 17 00:00:00 2001 From: Gerard Lanois Date: Mon, 9 Mar 2026 09:54:33 -0700 Subject: [PATCH 3/4] #72 Work in progress. First apparently working version. --- python/ppt/tests/testwc.py | 8 ++++---- python/ppt/wc.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/python/ppt/tests/testwc.py b/python/ppt/tests/testwc.py index 15bea1b..52a101c 100644 --- a/python/ppt/tests/testwc.py +++ b/python/ppt/tests/testwc.py @@ -92,8 +92,8 @@ def test_wc_file(self): { 'sw' : '--chars', 'val' : 1456 }, { 'sw' : '-l', 'val' : 25 }, { 'sw' : '--lines', 'val' : 25 }, - { 'sw' : '-w', 'val' : 302 }, - { 'sw' : '--words', 'val' : 302 }] + { 'sw' : '-w', 'val' : 264 }, + { 'sw' : '--words', 'val' : 264 }] for f in fixture: # Capture stdout when running. @@ -123,7 +123,7 @@ def test_wc_file2(self): # Compare the results. captured_stdout = io_out.getvalue() vals_int = [int(v) for v in captured_stdout.split()[0:3]] - expected_vals = [25, 302, 1464] + expected_vals = [25, 264, 1464] self.assertEqual(vals_int, expected_vals) @@ -156,6 +156,6 @@ def test_wc_stdin(self): # Compare the results. captured_stdout = io_out.getvalue() vals_int = [int(v) for v in captured_stdout.split()[0:3]] - expected_vals = [25, 302, 1456] + expected_vals = [25, 264, 1456] self.assertEqual(vals_int, expected_vals) diff --git a/python/ppt/wc.py b/python/ppt/wc.py index 0b83644..f24fad5 100644 --- a/python/ppt/wc.py +++ b/python/ppt/wc.py @@ -63,7 +63,7 @@ def wordcounttext(options, text, filepath): 'count' : text.count('\n'), 'requested' : options.lines }, 'words' : { - 'count' : len(re.findall(r"[\w']+|[.,!?;]", text)), + 'count' : len(re.findall(r'[^\s]+', text)), 'requested' : options.words} }, 'filepath' : filepath } From 66497dde0d27d6734c33d3703dd79e6f9583d96d Mon Sep 17 00:00:00 2001 From: Gerard Lanois Date: Sat, 14 Mar 2026 17:17:55 -0700 Subject: [PATCH 4/4] #72 Use underscores in function names for readability. --- python/ppt/wc.py | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/python/ppt/wc.py b/python/ppt/wc.py index f24fad5..f2544c7 100644 --- a/python/ppt/wc.py +++ b/python/ppt/wc.py @@ -30,7 +30,7 @@ import sys -def countrequested(count): +def count_requested(count): num_requested = 0 for (k, v) in list(count['requests'].items()): if v['requested']: @@ -38,7 +38,7 @@ def countrequested(count): return num_requested -def wordcounttext(options, text, filepath): +def word_count_text(options, text, filepath): # For stdin (filepath == ''), just use the length of # the incoming text. bytes_count = len(text) @@ -67,7 +67,7 @@ def wordcounttext(options, text, filepath): 'requested' : options.words} }, 'filepath' : filepath } - if countrequested(count) == 0: + if count_requested(count) == 0: # Nothing explicitly requested. Therefore, request lines, # words, and bytes. count['requests']['lines']['requested'] = True @@ -77,13 +77,13 @@ def wordcounttext(options, text, filepath): return count -def wordcountfile(options, filepath): +def word_count_file(options, filepath): with open(filepath, 'r', encoding='utf-8') as fin: text = fin.read() - return wordcounttext(options, text, filepath) + return word_count_text(options, text, filepath) -def measurewidth(count): +def measure_width(count): width = 0 # Find the width of the widest requested count. for v in count['requests'].values(): @@ -92,7 +92,7 @@ def measurewidth(count): return width -def wordcountfilenames(options): +def word_count_filenames(options): total = { 'bytes' : { 'count' : 0, @@ -111,8 +111,8 @@ def wordcountfilenames(options): widths = [] counts = [] for filepath in options.filenames: - count = wordcountfile(options, filepath) - width = measurewidth(count) + count = word_count_file(options, filepath) + width = measure_width(count) counts.append(count) widths.append(width) for k in count['requests'].keys(): @@ -123,10 +123,10 @@ def wordcountfilenames(options): return counts, widths, total -def printcountsbyfile(count, width): +def print_counts_by_file(count, width): # Have to do some work to duplicate the way wc spaces the numbers. - if countrequested(count) == 1: + if count_requested(count) == 1: # 1. When only one command line switch count requested, there are no leading spaces. if count['requests']['lines']['requested']: print(f"{count['requests']['lines']['count']} {count['filepath']}") @@ -163,23 +163,23 @@ def main(options): filenames = sys.stdin.read() # Split on NUL and throw away last one due to last NUL terminator. options.filenames = filenames.split('\x00')[:-1] - counts, widths, total = wordcountfilenames(options) + counts, widths, total = word_count_filenames(options) else: # Process data directly from stdin. text = sys.stdin.read() - count = wordcounttext(options, text, '') - width = measurewidth(count) - printcountsbyfile(count, width) + count = word_count_text(options, text, '') + width = measure_width(count) + print_counts_by_file(count, width) elif len(options.filenames) == 1: # Just one file on the command line. - count = wordcountfile(options, options.filenames[0]) - width = measurewidth(count) - printcountsbyfile(count, width) + count = word_count_file(options, options.filenames[0]) + width = measure_width(count) + print_counts_by_file(count, width) else: # Multiple files. - counts, widths, total = wordcountfilenames(options) + counts, widths, total = word_count_filenames(options) for count in counts: - printcountsbyfile(count, max(widths)) + print_counts_by_file(count, max(widths)) totals = [] for c in ('lines', 'words', 'chars', 'bytes'):