Coding 2022-04-17
Okay, I'm trying something a bit different, where I had a short mastodon thread that I was writing as I worked on the code, and I'll start off this post with the end result, and talk about it some.
@_validators.RejectBecause
def _path_is_well_behaved(path: pathlib.Path) -> typing.Iterator[str]:
if path.is_absolute():
yield f"The following path is absolute: {path}"
if any(part == os.path.pardir for part in path.parts):
yield (
f"The following path includes parent or current directory parts:"
f" {path}"
)
@attr.dataclass(frozen=True)
class PathWith(typing.Generic[T_co]):
path: pathlib.Path = attr.field(validator=_path_is_well_behaved)
val: T_co
So, first observation: this isn't much code. This might be from getting used to the different process, but there's also the fact that today was just kind of tiring, even if I did get some good relaxation in. I'm going to keep this up for now, and hope for better results next weekend. If that doesn't work out, I'll reconsider things.
So, that out of the way, the next question to ask is, what is most of this?
From the top:
- The _validators module is a local helper file I wrote to streamline writing validators for attrs. The RejectBecause class is a decorator that wraps a function that checks a value for reasons not to use it in the validated field. I'm still feeling things out with naming conventions around it. In another module, I've got it used in the attr.field() argument instead of as a decorator, so the functions are named by what they return. Here, it's being used as a decorator, so I named the function after the overall logic of the validator.
- The particular function here, _path_is_well_behaved, performs a few checks regarding the paths being associated with values. The first check is that the path is relative; in fact, it might be relative to an unknown starting point, which is why I didn't use Path.resolve() anywhere in this. The second check makes sure there are no parts of the path that are parent separators. This is not robust in the face of symlinks, and frankly, I don't care.
- Lastly, PathWith[T_co] is going to be one of the building blocks of my replacement system for building up environment information. This is a little involved, so let's break out of the bullets.
Currently, the Installer protocol only has one implementation, which uses virtualenv and pip together. This class bundles together "what arguments get passed to pip" and "what arguments get passed to virtualenv", which turns out to be the wrong thing to do, at least for how I'm using the Installer protocol. The problem is, nothing in this system allows for multiple Python versions to be selected, which is a pretty major feature. My goal is to pull things out into a protocol for setting up the environment, and a protocol for installing stuff in the environment. (I've thought so much about trying to make this idea generic in the type of installer, but I've just confused myself, so I need a simplified prototype I can tweak.) These protocols would not be concerned with the question of "where is this done on the file system"? Instead, instance of each would be put in a PathWith[T_co], and helper functions would be written to process that concrete code, independent of how any of the protocol implementations would work; they would simply receive the calculated environment path.
So, when I pick this back up, I can write those protocols, then the helper functions to consume them, then implement the protocols, then sub the new protocols in, and chase type errors until mypy thinks it should all work. If I find myself messing with typed mappings, I will take that excuse to start using that helper class in earnest.
For now, though, I just want to publish this entry and cap off the thread.
Good night.