🎓 A1Learner
🔍

Python Project: A Tested Little Library

A library is a small package that other projects can import. textstats uses a src layout, with the package under src.

The project tree

One folder holds the whole project. The package and the tests sit side by side.

textstats-project/
    pyproject.toml
    src/
        textstats/
            __init__.py
            core.py
    tests/
        test_textstats.py
    fresh/
        use_library.py

The pyproject.toml file

This small file names the project and says the packages live under src.

[project]
name = "textstats"
version = "0.1.0"
description = "Tiny text statistics helpers"
requires-python = ">=3.9"

[tool.setuptools.packages.find]
where = ["src"]

The public API in __init__.py

Users import these three names. Helper details stay inside core.py.

from .core import word_count, char_count, most_common_word

__all__ = ["word_count", "char_count", "most_common_word"]

One logger for the library

getLogger makes or finds a logger by name. Libraries usually take the module name.

import logging

logger = logging.getLogger("textstats.core")
print(logger.name)

The doctest inside the package

Run this from the project root, with src on the path. The counts come back exact.

import doctest
import textstats.core

print(doctest.testmod(textstats.core))

Example

# File: pyproject.toml
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"

[project]
name = "textstats"
version = "0.1.0"
description = "Tiny text statistics helpers"
requires-python = ">=3.9"

[tool.setuptools.packages.find]
where = ["src"]

# File: src/textstats/core.py
import logging

logger = logging.getLogger(__name__)

def word_count(text):
    """
    Count the words in a text.

    >>> word_count("one two three")
    3
    >>> word_count("")
    0
    """
    count = len(text.split())
    logger.debug("word_count counted %d words", count)
    return count

def char_count(text):
    return len(text)

def most_common_word(text):
    words = text.lower().split()
    if not words:
        return ""
    best = max(words, key=words.count)
    logger.info("most_common_word picked %s", best)
    return best

# File: src/textstats/__init__.py
from .core import word_count, char_count, most_common_word

__all__ = ["word_count", "char_count", "most_common_word"]

# File: tests/test_textstats.py
import unittest
from textstats import word_count, char_count, most_common_word

class TestTextStats(unittest.TestCase):
    def test_word_count(self):
        self.assertEqual(word_count("one two three"), 3)

    def test_char_count(self):
        self.assertEqual(char_count("hello"), 5)

    def test_most_common_word(self):
        self.assertEqual(most_common_word("cat dog cat"), "cat")

    def test_most_common_word_empty(self):
        self.assertEqual(most_common_word(""), "")

if __name__ == "__main__":
    unittest.main()

# File: fresh/use_library.py
import io
import unittest
from pathlib import Path

import textstats
from textstats import word_count, most_common_word

print("Imported the textstats library")
print("Words in sample:", word_count("the cat sat still"))
print("Most common word:", most_common_word("cat dog cat bird cat"))

tests_folder = str(Path(__file__).parent.parent / "tests")
suite = unittest.TestLoader().discover(tests_folder)
stream = io.StringIO()
result = unittest.TextTestRunner(stream=stream, verbosity=0).run(suite)
print("Tests run:", result.testsRun)
print("All passed:", result.wasSuccessful())

Key points

  • Package: a folder of modules that Python imports as one unit.
  • Src layout: the package lives in a src folder, separate from the tests.
  • pyproject.toml: the one file that states the project name and its build rules.
  • Use __init__.py to choose the public API, the names other code may rely on.
  • Logging: writing progress notes from inside code, instead of print.
  • A library logs and stays quiet.
  • The app that uses it decides what to show.
  • A test suite checks the library.

In this example, the five marked parts are five separate files. Build them into the tree from the first snippet. The core module holds the functions, one logger, and a doctest in word_count. The __init__ file re-exports the three public names. From the project root, run the suite with PYTHONPATH=src python3 -m unittest discover -s tests. It reports four tests and OK, with only the time in seconds varying. Then move into the fresh folder and run PYTHONPATH=../src python3 use_library.py. That folder holds no copy of the library, so the import proves the package works from outside. The script calls one function, then runs the suite quietly and prints the counts. Every printed line is the same on every run.

Note

The full logging tour lives in the logging section.

Here you only meet one logger and two calls.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Python Playground

Exercise

What does the __init__.py file do in this library?