Fix pylint(no-member) when accessing resource.id (#4813)

Pylint currently reports `E1101: Instance of 'Bucket' has no 'id' member (no-member)` on lines in Pulumi Python programs like:

```python
pulumi.export('bucket_name', bucket.id)
```

Here's a description of this message from http://pylint-messages.wikidot.com/messages:e1101:

> Used when an object (variable, function, …) is accessed for a non-existent member.
>
> False positives: This message may report object members that are created dynamically, but exist at the time they are accessed.

This appears to be a false positive case: `id` isn't set in the constructor (it's set later in `register_resource`) and Pylint isn't able to figure this out statically. `urn` has the same problem. (Oddly, Pylint doesn't complain when accessing other resource output properties).

This change refactors `register_resource` so that `id` and `urn` can be assigned in the resource's constructor, so that Pylint can see it being assigned. The change also does the same with `read_resource`.
This commit is contained in:
Justin Van Patten 2020-06-12 19:41:56 +00:00 committed by GitHub
parent 1357be4afb
commit 9b0169be35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 138 additions and 18 deletions

View file

@ -13,6 +13,9 @@ CHANGELOG
[#4812](https://github.com/pulumi/pulumi/pull/4812)
- Fix `pylint(no-member)` when accessing `resource.id`.
[#4813](https://github.com/pulumi/pulumi/pull/4813)
## 2.4.0 (2020-06-10)
- Turn program generation NYIs into diagnostic errors
[#4794](https://github.com/pulumi/pulumi/pull/4794)

View file

@ -18,7 +18,7 @@ from typing import Optional, List, Any, Mapping, Union, Callable, TYPE_CHECKING,
import copy
from .runtime import known_types
from .runtime.resource import register_resource, register_resource_outputs, read_resource
from .runtime.resource import _register_resource, register_resource_outputs, _read_resource
from .runtime.settings import get_root_resource
from .metadata import get_project, get_stack
@ -706,9 +706,18 @@ class Resource:
if not custom:
raise Exception(
"Cannot read an existing resource unless it has a custom provider")
read_resource(cast('CustomResource', self), t, name, props, opts)
res = cast('CustomResource', self)
result = _read_resource(res, t, name, props, opts)
res.urn = result.urn
assert result.id is not None
res.id = result.id
else:
register_resource(self, t, name, custom, props, opts)
result = _register_resource(self, t, name, custom, props, opts)
self.urn = result.urn
if custom:
assert result.id is not None
res = cast('CustomResource', self)
res.id = result.id
def _convert_providers(self, provider: Optional['ProviderResource'], providers: Optional[Union[Mapping[str, 'ProviderResource'], List['ProviderResource']]]) -> Mapping[str, 'ProviderResource']:
if provider is not None:

View file

@ -139,10 +139,21 @@ async def prepare_resource(res: 'Resource',
aliases,
)
class _ResourceResult(NamedTuple):
urn: Output[str]
"""
The URN of the resource.
"""
id: Optional[Output[str]] = None
"""
The id of the resource, if it's a CustomResource, otherwise None.
"""
# pylint: disable=too-many-locals,too-many-statements
def read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', opts: 'ResourceOptions'):
def _read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', opts: 'ResourceOptions') -> _ResourceResult:
if opts.id is None:
raise Exception(
"Cannot read resource whose options are lacking an ID value")
@ -162,7 +173,7 @@ def read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', op
urn_secret.set_result(False)
resolve_urn = urn_future.set_result
resolve_urn_exn = urn_future.set_exception
res.urn = known_types.new_output({res}, urn_future, urn_known, urn_secret)
result_urn = known_types.new_output({res}, urn_future, urn_known, urn_secret)
# Furthermore, since resources being Read must always be custom resources (enforced in the
# Resource constructor), we'll need to set up the ID field which will be populated at the end of
@ -174,7 +185,7 @@ def read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', op
resolve_value: asyncio.Future[Any] = asyncio.Future()
resolve_perform_apply: asyncio.Future[bool] = asyncio.Future()
resolve_secret: asyncio.Future[bool] = asyncio.Future()
res.id = known_types.new_output(
result_id = known_types.new_output(
{res}, resolve_value, resolve_perform_apply, resolve_secret)
def do_resolve(value: Any, perform_apply: bool, exn: Optional[Exception]):
@ -263,16 +274,23 @@ def read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', op
asyncio.ensure_future(RPC_MANAGER.do_rpc("read resource", do_read)())
return _ResourceResult(result_urn, result_id)
def read_resource(res: 'CustomResource', ty: str, name: str, props: 'Inputs', opts: 'ResourceOptions') -> None:
result = _read_resource(res, ty, name, props, opts)
res.urn = result.urn
assert result.id is not None
res.id = result.id
# pylint: disable=too-many-locals,too-many-statements
def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props: 'Inputs', opts: Optional['ResourceOptions']):
"""
registerResource registers a new resource object with a given type t and name. It returns the
auto-generated URN and the ID that will resolve after the deployment has completed. All
properties will be initialized to property objects that the registration operation will resolve
at the right time (or remain unresolved for deployments).
"""
def _register_resource(res: 'Resource',
ty: str,
name: str,
custom: bool,
props: 'Inputs',
opts: Optional['ResourceOptions']) -> _ResourceResult:
log.debug(f"registering resource: ty={ty}, name={name}, custom={custom}")
monitor = settings.get_monitor()
@ -289,17 +307,17 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
urn_secret.set_result(False)
resolve_urn = urn_future.set_result
resolve_urn_exn = urn_future.set_exception
res.urn = known_types.new_output({res}, urn_future, urn_known, urn_secret)
result_urn = known_types.new_output({res}, urn_future, urn_known, urn_secret)
# If a custom resource, make room for the ID property.
result_id = None
resolve_id: Optional[Callable[[
Any, bool, Optional[Exception]], None]] = None
if custom:
res = cast('CustomResource', res)
resolve_value: asyncio.Future[Any] = asyncio.Future()
resolve_perform_apply: asyncio.Future[bool] = asyncio.Future()
resolve_secret: asyncio.Future[bool] = asyncio.Future()
res.id = known_types.new_output(
result_id = known_types.new_output(
{res}, resolve_value, resolve_perform_apply, resolve_secret)
def do_resolve(value: Any, perform_apply: bool, exn: Optional[Exception]):
@ -433,6 +451,22 @@ def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props:
asyncio.ensure_future(RPC_MANAGER.do_rpc(
"register resource", do_register)())
return _ResourceResult(result_urn, result_id)
def register_resource(res: 'Resource', ty: str, name: str, custom: bool, props: 'Inputs', opts: Optional['ResourceOptions']) -> None:
"""
Registers a new resource object with a given type t and name. It returns the
auto-generated URN and the ID that will resolve after the deployment has completed. All
properties will be initialized to property objects that the registration operation will resolve
at the right time (or remain unresolved for deployments).
"""
result = _register_resource(res, ty, name, custom, props, opts)
res.urn = result.urn
if custom:
assert result.id is not None
res = cast('CustomResource', res)
res.id = result.id
def register_resource_outputs(res: 'Resource', outputs: 'Union[Inputs, Output[Inputs]]'):
async def do_register_resource_outputs():

View file

@ -1738,3 +1738,36 @@ func TestLargeResourceDotNet(t *testing.T) {
Dir: filepath.Join("large_resource", "dotnet"),
})
}
// Test to ensure Pylint is clean.
func TestPythonPylint(t *testing.T) {
var opts *integration.ProgramTestOptions
opts = &integration.ProgramTestOptions{
Dir: filepath.Join("python", "pylint"),
Dependencies: []string{
filepath.Join("..", "..", "sdk", "python", "env", "src"),
},
ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) {
randomURN := stack.Outputs["random_urn"].(string)
assert.NotEmpty(t, randomURN)
randomID := stack.Outputs["random_id"].(string)
randomVal := stack.Outputs["random_val"].(string)
assert.Equal(t, randomID, randomVal)
cwd := stack.Outputs["cwd"].(string)
assert.NotEmpty(t, cwd)
pylint := filepath.Join("venv", "bin", "pylint")
if runtime.GOOS == WindowsOS {
pylint = filepath.Join("venv", "Scripts", "pylint")
}
err := integration.RunCommand(t, "pylint", []string{pylint, "__main__.py"}, cwd, opts)
assert.NoError(t, err)
},
Quick: true,
UseAutomaticVirtualEnv: true,
}
integration.ProgramTest(t, opts)
}

View file

@ -0,0 +1,5 @@
*.pyc
/.pulumi/
/dist/
/*.egg-info
venv/

View file

@ -0,0 +1,3 @@
name: pylint
description: A simple Python program that should be pylint clean.
runtime: python

View file

@ -0,0 +1,32 @@
# Copyright 2016-2020, Pulumi Corporation. All rights reserved.
"""An example program that should be Pylint clean"""
import binascii
import os
import pulumi
from pulumi.dynamic import Resource, ResourceProvider, CreateResult
class RandomResourceProvider(ResourceProvider):
"""Random resource provider."""
def create(self, props):
val = binascii.b2a_hex(os.urandom(15)).decode("ascii")
return CreateResult(val, {"val": val})
class Random(Resource):
"""Random resource."""
val: str
def __init__(self, name, opts=None):
super().__init__(RandomResourceProvider(), name, {"val": ""}, opts)
r = Random("foo")
pulumi.export("cwd", os.getcwd())
pulumi.export("random_urn", r.urn)
pulumi.export("random_id", r.id)
pulumi.export("random_val", r.val)

View file

@ -0,0 +1 @@
pylint