Skip to content

← Seams, Modules, Packaging and Tooling step 27 of 36

Medium Primitives

Parsing an entry-point object reference

Implement the parser an installer runs over every [project.scripts] line before it can generate a wrapper.

ObjectRef = tuple[str, tuple[str, ...], tuple[str, ...]]

def parse_entry_point(spec: str) -> ObjectRef:

Return (module, attrs, extras). The grammar, which you should implement exactly as written here:

entry-point = module [ ":" attrs ] [ extras ]
module      = identifier ("." identifier)*
attrs       = identifier ("." identifier)*
extras      = "[" extra ("," extra)* "]"
identifier  = [A-Za-z_][A-Za-z0-9_]*
extra       = [A-Za-z0-9_][A-Za-z0-9._-]*

Whitespace is permitted around the whole reference, around the :, and inside the brackets. Anything that does not match — an empty module, an empty attribute path, a segment that is not an identifier, an unbalanced bracket, an empty extras list — raises ValueError.

Examples:

Input Result
"pkg.mod" ("pkg.mod", (), ())
"pkg.mod:Class.method" ("pkg.mod", ("Class", "method"), ())
"pkg.mod:main [extra1,extra2]" ("pkg.mod", ("main",), ("extra1", "extra2"))
":main" ValueError
"pkg.mod:" ValueError

Because the harness compares return values rather than exceptions, the graded entrypoint is a driver:

def solve(spec: str) -> tuple[Literal["ok", "invalid"], str, tuple[str, ...], tuple[str, ...]]:

("ok", module, attrs, extras) on success, and ("invalid", "", (), ()) when parse_entry_point raises. Write the real function properly and let the driver catch — the point is that the parser has one job and reports failure by raising, not by returning a sentinel that every caller must remember to check.

Why validation, and not just split(":"). This string ends up inside a generated wrapper script that an installer writes into someone’s bin/ directory. A malformed reference that is accepted here becomes an executable that fails at invocation with an import error, on a user’s machine, long after anything can be done about it. Rejecting "pkg-mod:main" at build time costs nothing; accepting it costs a bug report.

Returns a 4-tuple containing two tuples, not lists. The harness compares container types exactly, which is the point: a caller doing getattr down attrs needs a fixed-length sequence, not something it can accidentally mutate.

Your submission must pass mypy --strict, including the Literal tag on the driver’s return type.