Coding 2021-11-14

By Max Woerner Chase

Feeling kind of out of it, but I think making some progress on MOTR will help. So, the goal right now is to write some prototype code from a usage perspective.

Let's take a look at the simple cases.

The name of a command requires a non-empty sequence, which it only really makes sense to have as strings. (The first string gets converted to an Input as part of processing.) The only other CmdMeta field to cover is req_set, which is pretty necessary in every case I can think of. Beyond that, it needs the ability to customize every other Fragment argument. With that in mind, I can't think of any sensible shortcut for creating these, since it's just going to need to forward (nearly) all of the arguments. The stuff that it makes sense to shortcut is the CmdMeta. Maybe something like:

# The linewrapping is aggressive
# because I'm trying to keep the
# width of the code block boxes in
# mind.
def base_cmd(
    cmd: str,
    *sub_cmds: str,
    req_set: RequirementSet = frozenset(),
) -> CmdMeta:
    return CmdMeta(
        (cmd, *sub_cmds),
        req_set=req_set,
    )

def static_option(
    *args: str,
) -> Option[typing.Any]:
    return Option(CmdMeta(args))

Pytest = Literal["pytest"]

PYTEST_BASE: Fragment[
    Module, Pytest
] = Fragment(
    base_cmd(
        "pytest",
        req_set=frozenset([
            ("pip", "pytest")
        ])
    ),
    allowed_codes=frozenset([1]),
)

NO_CACHE: Option[
    Pytest,
] = static_option(
    "-p", "no:cacheprovider"
)

PYTEST_DEFAULT: Fragment[
    Module, Pytest
] = PYTEST_BASE.with_option(
    NO_CACHE,
)

There are some improvements that could be made to this code from a conciseness perspective. For example, if the pytest stuff were in it own module, then that module could define local type aliases for Fragment and Option to fill in the type arguments. Things start to get interesting for the other options. Also, there should really be a static builder wrapped around that stuff... So maybe the last bit should be more like

PYTEST_DEFAULT_BUILDER: Builder[
    Fragment[Module, Pytest]
] = static_builder(
    PYTEST_BASE.with_option(
        NO_CACHE,
    )
)

But the other options have to be embedded in Builders...

# Avoid too much flexibility to start with.
TESTDIR_BUILDER: Builder[
    Option[Pytest]
] = Builder(
    build=lambda sreg, oreg:
        static_option(
            oreg["package"].root
            / "tests"
        ),
    keys=frozenset(["package"])
)

That would really benefit from a decorator helper, to use instead of the lambda. I'd also really like to nail down a stricter interface for the registries. Let's see if I can get anywhere when it comes to the output files.

I'd like to see a single object that can construct either of the following:

XUNIT_OUT_BUILDER: Builder[
    Option[Pytest]
] = Builder(
    build=lambda sreg, oreg: Option(
        CmdMeta(
            args=(
                "--junitxml",
                Output(
                    REPORTS
                    / oreg["segments"]
                    / oreg["package"].root
                    / "junit.xml"
                )
            )
        )
    ),
    keys=frozenset([
        "package", "segments"
    ]),
    maximal=True,
)

XUNIT_IN_BUILDER: Builder[
    Option[JunitMerge]
] = Builder(
    build=lambda sreg, oreg: Option(
        CmdMeta(args=tuple(
            Input(
                REPORTS
                / oreg["segments"]
                / package.root
                / "junit.xml"
            )
            for package
            in sreg["package"]
        ))
    ),
    keys=frozenset(["segments"]),
)

Writing this out is making me really want to have the registries work with typed keys, but I'll put a pin in that. Now, because the types of these builders have to be different, that implies to me that there must be some kind of callback involved, which is also setting up the prefix. The types can't really elegantly control the Input/Output distinction or the maximal field, which implies to me that these are two related methods that get passed similar callbacks. One method generates Inputs, the other generates Outputs. Either that, or there's a flag to control what's produced, and the flag defaults to Input. To handle it correctly in either case, the output case needs a callback to determine the names of the Output, if any.

The other possible way to handle the types is to... actually, I thought of something promising in the middle of this sentence. Let's give it a shot.

XUNIT_META_BUILDER = Builder(
    builder=lambda sreg, oreg: Input(
        REPORTS
        / oreg["segments"]
        / oreg["package"].root
        / "junit.xml"
    ),
    keys=frozenset(["segments", "package"]),
)

XUNIT_OUT_BUILDER = XUNIT_META_BUILDER.meta_build(
    prefix=PYTEST_JUNITXML, # Option[Pytest]
    keys=frozenset(["package", "segments"]),
    output_names=(),
)

XUNIT_IN_BUILDER = XUNIT_META_BUILDER.meta_build(
    prefix=JUNIT_MERGE_NOOP, # Option[JunitMerge], empty
    keys=frozenset(["segments"]),
)

I'm not sure it'll make sense to use Builder[Input] for this, but the types match up. (I mean, it's actually Builder[Input[T]] like that, and maybe it can be constrained to Builder[Input[PathStr]].)

Anyway, this has been pretty enlightening. Let's see what I need.

So, notes on the proper way to invoke builders:

They should somehow create a bunch of prefix requirements. My inclination is that they're generators with return values, so those all get composed together, then the top-level return value gets processed into another requirement. To get this right, I'll need to nail those details down, and possibly add fields to Builder, before I design meta_build.

Anyway, it's late and I extremely need to sleep.

Good night.