Hi, I am trying to build a plugin to execute some...
# plugins
t
Hi, I am trying to build a plugin to execute some commands. What I basically need is to run
poetry version type=patch
(patch can be “major”, “minor” or “patch). This in turn will modify the
pyproject.toml
version number. I created this rule:
Copy code
@rule
async def get_project_version_file_view(
    target: ProjectVersionTarget,
) -> ProjectVersionFileView:
    pex = await Get(
        Pex,
        PexRequest(
            output_filename="poetry.pex",
            internal_only=True,
            requirements=PexRequirements(["poetry"]),
            main=ConsoleScript("poetry"),
        ),
    )
    result = await Get(
        FallibleProcessResult,
        PexProcess(
            pex,
            argv=["version"],
            description="get poetry version",
            working_directory=".",
        ),
    )

    <http://logger.info|logger.info>(result.stdout.decode())
    <http://logger.info|logger.info>(result.stderr.decode())

    return ProjectVersionFileView(path="path", version=result.stdout.decode())
Whoever, it does look like it can’t find the pyproject.toml file as I get this error:
Copy code
Poetry could not find a pyproject.toml file in /private/var/folders/0g/7yz8qfln279gr8gtr874hbvr1wzvx2/T/pants-sandbox-BxA03N or its parents
Is there a way to ensure that pyproject.toml file is there? In this use case I am only looking to get the version from the command, but I would like to update the version passing arguments. Mentioning this to know if I am doing something not intended and getting down a rabbit hole. Thanks in advance for your help!
h
Interesting plugin! Others might benefit from it if you're open to publishing it when it's done. The best way to start debugging stuff like this is to run with
--keep-sandboxes=on_failure
, which will print the location of the sandbox of the failed process so you can look inside it.
When you do, you'll probably see that pyproject.toml isn't there
So the question now is, what is
ProjectVersionTarget
?
You're not setting the input digest on your
PexProcess
, so it's running in a mostly empty sandbox (other than the poetry pex)
You have to make sure you bring pyproject.toml into the sandbox
Typically because you either run your new goal directly on the pyproject.toml or on something that depends on it
and you pull in all the transitive deps of whatever you're running on
(Also note that only your @goal_rule can actually write new/modified files to the workspace. So you'll have to capture the changes from your process (via the output_files arg) and pass them back up to the goal_rule, but that's for later)
But anyway, the important thing here is, how are you running your plugin? Custom goal, I assume?
You probably want to do something like
pants bump-version path/to/pyproject.toml
?
👍 1
t
Is there any example around there or docs I can follow? partly following the tutorial in here https://github.com/pantsbuild/pants/blob/main/docs/markdown/Tutorials/create-a-new-goal.md The ProjectVersionTarget is this:
Copy code
class ProjectVersionTarget(Target):
    alias = "versioned_project"
    description = "A versioned project target where the pyproject.toml version is updated via the `poetry version ..` command"
    core_fields = (*COMMON_TARGET_FIELDS, BumpVersionTypeField)
    tags = ["bump", "versioned"]
    help = "This target indicates this project will update the pyproject.toml version by using the `poetry version $(type)` command, where the type can be `major`, `minor` or `patch` (default)"
So the BUILD file uses this macro (as I got a few pyproject.toml) :
Copy code
# flake8: noqa: F821
def poetry_distribution(name, **kwargs):
    resources(name="package_data", sources=["pyproject.toml", "README.md"])

    versioned_project(
        name=f"{name}_version",
        tags=["version", "bump"],
    )

    python_distribution(
        name="dist",
        dependencies=[":package_data", f"src/python/{name}/src", "//:root"],
        repositories=["<https://nexus3.tl.pplaws.com/repository/pypi-ppl/>"],
        provides=python_artifact(name=f"{name}"),
        generate_setup=False,
    )
again, if you can point me either to some docs or bits that I need to put together, I can see how far I get on. Really happy to share the final working example. At the moment is a bit of a WIP, but if you say it should work, I can try to get it more or less working and see what bit I get stuck with
thanks. I the meantime i will see what I can take from your previous messages. You have probably already told me how to do it, I just need to get my head around on how this all works together. 😄 Thanks again!
and yes, the command I plan to use is
./pants bump-version :: type=patch
with the
--changed-since=main
so only the projects that changed get their version updated.
but initially, just having a
./pants bump-version
that prints what each pyproject.toml version (with the
poetry version
command, will do. As that validates the pyproject.toml file is there and the command is reading it for each of the pyproject.toml I got
f
The plugin docs are generally where you want to be looking.
From a design perspective, first question: Do you just want to reuse the
resources
target generated for the
pyproject.toml
by the
poetry_distribution
macro?
in your
bump-version
goal rule, add a
targets: Targets
parameter to receive the targets to operate on. This will be set regardless of whether the goal was invoked with explicit targets or via
--changed-since
then filter the targets to just
resource
targets:
[tgt for tgt in targets if tgt.has_field(ResourceSourceField)]
then retrieve the
ResourceSourceField
to find the ones with a
pyproject.toml
as the source
you can then obtain a input
Digest
using the
HydrateSourcesRequest
type
the core principle of the Pants engine is manipulating the filesystem indirectly by using the
Digest
type to represent a filesystem tree
then to write a new version of the file, your helper rule should return a new
Digest
with the modified files
then your goal rule can use the
Workspace
type and its
write_digest
method to write the output back out to the repository
you may want to take a look at the
go-generate
custom goal and how it invokes
go generate
and then writes the outputs back to the repository
👍 1
t
Nice, I think I was missing the HydrateSourcesRequest but will see if I get time tomorrow between meetings. Otherwise Thursday… but I am determine to make this work 😄 Thanks again!
t
Hi @fast-nail-55400, got some where near there.. but the last bit. I put the code in here to see if you can help me on that last bit. That is saving the files from the Runtime environment to my actual folder. My macro looks like this:
Copy code
# flake8: noqa: F821
def poetry_distribution(name, **kwargs):
    resources(name="package_data", sources=["pyproject.toml", "README.md"])

    versioned_project(
        name=f"{name}_version",
        tags=["version", "bump"],
        source="pyproject.toml"
    )

    python_distribution(
        name="dist",
        dependencies=[":package_data", f"src/python/{name}/src", "//:root"],
        repositories=["<https://nexus3.tl.pplaws.com/repository/pypi-ppl/>"],
        provides=python_artifact(name=f"{name}"),
        generate_setup=False,
    )
I know you mentioned reusing the resources but will probably do that on a next version.
The classes that define the target/goal:
Copy code
class BumpVersionTypeField(StringField):
    help = "Type of version increase. Can be major, minor or patch"
    alias = "type"
    default = "patch"
    valid_choices = ("major", "minor", "patch")
    description = "Type of version increase. Can be major, minor or patch"
    required = False


class ProjectVersionTarget(Target):
    alias = "versioned_project"
    description = "A versioned project target where the pyproject.toml version is updated via the `poetry version ..` command"
    core_fields = (*COMMON_TARGET_FIELDS, BumpVersionTypeField, SingleSourceField)
    tags = ["bump", "versioned"]
    help = "This target indicates this project will update the pyproject.toml vesrion by using the `poetry version $(type)` command, where the type can be `major`, `minor` or `patch` (default)"


class ProjectVersionSubsystem(GoalSubsystem):
    name = "bump-version"
    help = (
        "Show representation of the project version from the `poetry version` command."
    )


class ProjectVersionGoal(Goal):
    subsystem_cls = ProjectVersionSubsystem
The goal_rule is:
Copy code
@goal_rule
async def goal_show_project_version(
    console: Console,
    targets: Targets,
    workspace: Workspace
) -> ProjectVersionGoal:
    projectVersionTargets = [tgt for tgt in targets if tgt.alias == ProjectVersionTarget.alias]
    results = await MultiGet(
        Get(Digest, ProjectVersionTarget, tgt)
        for tgt in projectVersionTargets
    )
    output_digest = await Get(Digest, MergeDigests([r for r in results]))
    workspace.write_digest(output_digest)
    return ProjectVersionGoal(exit_code=0)
and my rule looks like this:
Copy code
@rule
async def get_project_version_file_view(
    target: ProjectVersionTarget,
) -> Digest:
    sources = await Get(HydratedSources, HydrateSourcesRequest(target[SourcesField]))
    pex = await Get(
        Pex,
        PexRequest(
            output_filename="poetry.pex",
            internal_only=True,
            interpreter_constraints=InterpreterConstraints(["==3.9.*"]),
            requirements=PexRequirements(["poetry"]),
            main=ConsoleScript("poetry"),
        ),
    )
    result = await Get(
        ProcessResult,
        PexProcess(
            pex,
            argv=["version", "patch"],
            input_digest=sources.snapshot.digest,
            description="get poetry version",
            working_directory=target.residence_dir,
            level=LogLevel.DEBUG,
        ),
    )

    <http://logger.info|logger.info>(result.stdout.decode())
    <http://logger.info|logger.info>(result.stderr.decode())

    return result.output_digest
The code seems to do what it says on the tin, whoever is not saving back the files on my file system. Further info on structure is in here: https://github.com/pantsbuild/pants/issues/18179 Help please!! 😄 Also, the most interesting part I learned is the been able to debug with this command
Copy code
PANTS_DEBUG=1 ./pants bump-version :: --keep-sandboxes=always --no-pantsd
I will be happy to put a PR to update the documentation, whoever do you prefer it I add a new page in https://www.pantsbuild.org/docs/common-plugin-tasks or this one https://www.pantsbuild.org/docs/plugins-overview ? I think the former might make more sense.
f
You need to specify
output_files
or
output_directories
on the
Process
(in this case the
PexProcess
wrapper) otherwise Pants will not capture outputs
and with no capture, then
result.output_digest
will be the empty digest
You can debug situation like this by converting the
Digest
to a
Snapshot
:
snapshot = await Get(Snapshot, digest, result.output_digest)
and then log
snapshot.files
👍 1
t
thanks! I will try this next week. Hopefully that will be it.
f
It probably is. There's no output from
workspace.write_digest
because the
Process
was not configured to capture any output from running the pex.
t
It did work! Thanks for all your help. I will put a revised solution in the github issue once I tidy the code up. Thanks again!
Ok, I have put the code in the github issue, and a wee follow up question. https://github.com/pantsbuild/pants/issues/18179 Basically, the goal works whoever I would like it to trigger when/if the python source files change.
Copy code
./pants bump-version --changed-since=main --changed-dependees=transitive
I might be wrong, but should I have extended the pythonSource target instead of creating a new one? Can I “link” them somehow? Thanks in advance
👀 1
h
Thanks for posting this! I'll take a look
👍 1