Select Counts With Command-Line Options (Solution: Part 2)

In this lesson, you’ll finish off your implementation of the task from part one by incorporating the logic to selectively display the requested counts.

Know Your Starting Point

If you got lost while working on this task, then expand the section below to view the full source code of the wordcount command from part one:

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

    @property
    def max_digits(self):
        return len(str(max(self)))

    def as_string(self, max_digits):
        return (
            f"{self.lines:>{max_digits}} "
            f"{self.words:>{max_digits}} "
            f"{self.bytes:>{max_digits}}"
        )

    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
    for info in file_infos:
        line = info.counts.as_string(max_digits)
        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), "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())

While this code makes a few more acceptance criteria pass, most still keep failing. Don’t worry, though, as you’re on the right track. You’ve already done the hard work in part one!

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

Avatar image for davidbonn

davidbonn on June 20, 2025

Maybe it is just me but it seemed to be simpler to have the command line options represented as a list of strs, the strs being the attribute names I wanted to print, e.g. you could do:

ap.add_argument("--lines", action="append_const", const="n_lines", dest="options")
ap.add_argument("--words", action="append_const", const="n_words", dest="options")
ap.add_argument("--bytes", action="append_const", const="n_bytes", dest="options")
ap.add_argument("--chars", action="append_const", const="n_chars", dest="options")

And the code to generate the output would be approximately:

def output(self, options, width):
    values = []
    for thing in ["n_lines", "n_words", "n_chars", "n_bytes"]:
        if thing in options:
            values.append(getattr(self, thing))

    rc = " ".join([f"{v:>{width}d}" for v in values])

    if len(self.filename):
        rc += f" {self.filename}{self.message}"

    return rc
Avatar image for Bartosz Zaczyński

Bartosz Zaczyński RP Team on June 23, 2025

@davidbonn Hi David! Thank you for the feedback and sharing your code. It’s a perfectly valid solution, which certainly works well and keeps things simple!

Ultimately, there’s no single “best” way to solve the task. There are always different trade-offs depending on your design goals and preferences. The sample solution provided with this coding challenge is just one possible way to tackle it. Personally, I find the use of an IntFlag enumeration to be more extensible if more options or combinations are added later.

Thanks again for your input. It’s always great to see alternative perspectives!

Become a Member to join the conversation.