I have a test for a rule that is part of my in-hou...
# development
f
I have a test for a rule that is part of my in-house pants plugin. I can run the tests in 2.18 fine but in 2.19 there seem to be some change done with how rules are run as a call to a rule function now returns a coroutine:
E       AttributeError: 'coroutine' object has no attribute 'directory'
when attempting to access an object that was available when running with 2.18. The rule wasn't
async
and I was able to run a pytest test calling it directly
result = construct_existing_python_requirement_object(tgt)
in 2.18 inside the test case function body. With 2.19, however, I had to make the test pass by making the
@rule
async, calling it as
result = await construct_existing_python_requirement_object(tgt)
and making a pytest test case
async def test_case(...)
. Is there a way to avoid doing that and just keep things as they were? It's an extremely simple rule for which I don't want to involve rule runners (https://www.pantsbuild.org/2.19/docs/writing-plugins/the-rules-api/testing-plugins#approach-3-rulerunner-integration-tests-for-rules) as it just takes an object and returns a data class.
1
It must be something related to https://github.com/pantsbuild/pants/issues/19730
the release change log suggests
Some `@rule`s may now need to pass
canonical_name_suffix
in preparation for "call by name". (#19755)
b
A
@rule
that’s not
async
suggests it’s not calling any other rules/interacting with the pants scheduler, so maybe it can be a normal function without that decorator?
I might not know what’s happening here though. Can you share any of the code?
f
thanks, Huon! The troubling one is
construct_existing_python_requirement_object
.
Copy code
@rule(desc="Get a basic info object from a python_requirement target")
def construct_existing_python_requirement_object(target: PythonRequirementTarget) -> ExistingPythonRequirementInfo:
    return ExistingPythonRequirementInfo(
        directory=target.address.spec_path, project_name=target.get(PythonRequirementsField).value[0].project_name
    )


@rule(desc="Get basic info objects from all existing python_requirement targets")
async def determine_all_existing_python_requirement_targets(
    all_targets: AllUnexpandedTargets,
) -> AllExistingPythonRequirementTargets:
    results = await MultiGet(
        Get(ExistingPythonRequirementInfo, PythonRequirementTarget, target)
        for target in all_targets
        if target.has_field(PythonRequirementsField)
    )
    return AllExistingPythonRequirementTargets(results)
it's a bit dated, but IIRC I had to make it a rule because I was taking advantage of the
MultiGet
as in
Copy code
results = await MultiGet(
        Get(ExistingPythonRequirementInfo, PythonRequirementTarget, target)
        for target in all_targets
        if target.has_field(PythonRequirementsField)
    )
b
Ah okay. I think because this isn’t async and isn’t (transitively) executing external processes , there’s no concurrency possible and thus it behaves the same with MultiGet and without it (eg a normal list comprehension). So I think the “remove
@rule
“ idea may indeed be applicable
f
oh thank you. I've replaced
Copy code
results = await MultiGet(
        Get(ExistingPythonRequirementInfo, PythonRequirementTarget, target)
        for target in all_targets
        if target.has_field(PythonRequirementsField)
    )
with
Copy code
results = [construct_existing_python_requirement_object(target)
        for target in all_targets
        if target.has_field(PythonRequirementsField)
    ]
and it seemed to give me the expected results when running the plugin
b
Great!