Coding 2021-07-28

By Max Woerner Chase

As I thought more and more about how to handle multiple Python versions, I realized that I needed a concrete example to really ground things. So, I'm going to take the previous code, strip the comments out, and try to figure out the "proper" way to extend it for multiple versions.

@attr.dataclass(frozen=True)
class Flake8HTML:
    additional_args: typing.Tuple[str, ...]
    python_versions: typing.Tuple[typing.Optional[str], ...] = (None,)  # For checking syntax changes across Python versions, I guess.

    def __call__(self, projects: typing.Dict[str, Project] = {"": ROOT}) -> ChangeStream:
        for version in self.python_versions:
            report_dir = REPORTS / "flake8"

            if version is not None:
                report_dir /= version

            flake8 = yield from venv_wrapper("check", "flake8", "check", python=version)

            for project in projects.values():
                project_report_dir = project.suffix(report_dir)

                project_report_file = project_report_dir.path / "index.html"

                action_name = project.prefix("check")

                yield from mkdir(project_report_dir)
                yield from cmd(
                    [
                        flake8,
                        "--format=html",
                        "--htmldir",
                        project_report_dir,
                        *self.additional_args,
                        project.prefix("src"),
                        project.prefix("tests"),
                    ],
                    action_name,
                    allowed_codes={1},
                )
                yield from add_target(project_report_file, action_name, "check")

Most of the code is the same, but the differences are instructive. Because I'm generating a separate set of reports for each Python version, I can't do something like dependency-inject the virtual environment creation step, because then a bunch of registry entries collide. It's a little unfortunate that there's no "generic" way to specify Python versions, but something like a coverage run is going to invoke different environments at different "stages", and it's not clear to me how to separate that out, unless...

I just drew some inspiration from the fact that some of my projects use my limit-coverage tool and some don't. So, maybe I want to decompose some of these complicated runs into specialized types that nest each other in a carefully chosen way. If I enforce an idea of one (lexical) environment call per type, then however I specify the Python environment is unambiguous as to what it refers to. Because the projects should be the same over the course of a call, but each thing handling the call can vary its environments, I think what I want is to standardize on an attribute for everything containing a (lexical) call, or maybe some kind of accessor function? I'll have to try this out later, when I'm not wiped out from traveling or apprehensive about work.

It's pretty instructive how many of the ideas I had when I was thinking about "supporting multiple Python versions" in abstract terms, proved too unworkable to bother prototyping, when I took a look at actual (-ish) code.

I don't know when I'll be up for working on this again, so I think I should make myself take a break, at least until work calms down a little.

(But wait, if the "correct" implementation at that point is always a big for loop, then it should just be taking a single version, and the for loop should be lifted out to... somewhere. Or the version should be made part of the call interface, and then there's a wrapper type that composes a list of versions to make it more session-y. I guess I'm not done thinking about this.)

Good night.