Add Support for Counting the Characters (Solution)

In this lesson, you’ll walk through the process of adding support for the --chars flag to your wordcount command. This feature complements the existing functionality by allowing users to count characters in addition to lines, words, and bytes.

Know Your Starting Point

Previously, you constructed the core functionality of the wordcount command, allowing users to count lines, words, and bytes through a streamlined command-line interface. Before moving forward, make sure that your codebase looks as follows:

Python src/wordcount.py
import sys
from argparse import ArgumentParser, Namespace
from dataclasses import dataclass
from enum import IntFlag, auto
from functools import cached_property
from pathlib import Path
from typing import NamedTuple

class SelectedCounts(IntFlag):
    NONE = 0
    LINES = auto()
    WORDS = auto()
    BYTES = auto()
    DEFAULT = LINES | WORDS | BYTES

class Arguments(Namespace):
    @cached_property
    def selected_counts(self):
        selected = self.lines | self.words | self.bytes
        return selected or SelectedCounts.DEFAULT

class Counts(NamedTuple):
    lines: int = 0
    words: int = 0
    bytes: int = 0

    def max_digits(self, selected_counts):
        return len(str(max(self.numbers(selected_counts))))

    def as_string(self, max_digits, selected_counts):
        return " ".join(
            f"{number:>{max_digits}}"
            for number in self.numbers(selected_counts)
        )

    def numbers(self, selected_counts):
        return [
            getattr(self, flag.name.lower())
            for flag in SelectedCounts
            if flag & selected_counts
        ]

    def __add__(self, other):
        return Counts(
            lines=self.lines + other.lines,
            words=self.words + other.words,
            bytes=self.bytes + other.bytes,
        )

@dataclass(frozen=True)
class FileInfo:
    path: Path
    counts: Counts

    @classmethod
    def from_path(cls, path):
        if path.name == "-":
            raw_text = sys.stdin.buffer.read()
        elif path.is_file():
            raw_text = path.read_bytes()
        else:
            return cls(path, Counts())
        text = raw_text.decode("utf-8")
        return cls(
            path,
            Counts(
                lines=text.count("\n"),
                words=len(text.split()),
                bytes=len(raw_text),
            ),
        )

def main():
    args = parse_args()
    if len(args.paths) > 0:
        file_infos = [FileInfo.from_path(path) for path in args.paths]
    else:
        file_infos = [FileInfo.from_path(Path("-"))]
    total_counts = sum((info.counts for info in file_infos), Counts())
    max_digits = total_counts.max_digits(args.selected_counts)
    for info in file_infos:
        line = info.counts.as_string(max_digits, args.selected_counts)
        if info.path == Path("-"):
            print(line)
        elif not info.path.exists():
            print(line, info.path, "(no such file or directory)")
        elif info.path.is_dir():
            print(line, f"{info.path}/ (is a directory)")
        else:
            print(line, info.path)
    if len(file_infos) > 1:
        print(
            total_counts.as_string(max_digits, args.selected_counts), "total"
        )

def parse_args():
    parser = ArgumentParser()
    parser.add_argument("paths", nargs="*", type=Path)
    for flag in SelectedCounts:
        parser.add_argument(
            f"--{flag.name.lower()}",
            action="store_const",
            const=flag,
            default=SelectedCounts.NONE,
        )
    return parser.parse_args(namespace=Arguments())

The next step is to build on this by introducing character count functionality. This will be your last missing feature in this coding challenge.

Add Support for the --chars Flag

Thanks to the fact that you’ve built many robust and reusable abstractions, solving this task entails only a few minor tweaks. That said, be sure not to miss any of the crucial details. Below are the necessary adjustments you’ll need to implement in your code to solve this final task of the challenge.

First, you’re going to need to define another command-line option in your argument parser. Since parse_args() relies on the members of the SelectedCounts bit field, it’s sufficient to add a new member representing the --chars flag:

Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Already a member? Sign-In

Locked learning resources

The full lesson is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Lesson

Already a member? Sign-In

Become a Member to join the conversation.