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:
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: