Skip to content

Commit

Permalink
feat: add api for analysis-phase code
Browse files Browse the repository at this point in the history
  • Loading branch information
rickeylev committed Oct 3, 2024
1 parent 413690f commit 8a66b6e
Show file tree
Hide file tree
Showing 12 changed files with 292 additions and 23 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ A brief description of the categories of changes:
### Added
* (py_wheel) Now supports `compress = (True|False)` to allow disabling
compression to speed up development.
* (api) Added {obj}`merge_py_infos()` so user rules can merge and propagate
`PyInfo` without losing information.

### Removed
* Nothing yet
Expand Down
2 changes: 2 additions & 0 deletions docs/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -93,10 +93,12 @@ sphinx_stardocs(
"//python:py_runtime_info_bzl",
"//python:py_test_bzl",
"//python:repositories_bzl",
"//python/api:api_bzl",
"//python/cc:py_cc_toolchain_bzl",
"//python/cc:py_cc_toolchain_info_bzl",
"//python/entry_points:py_console_script_binary_bzl",
"//python/private:py_cc_toolchain_rule_bzl",
"//python/private/api:py_common_api_bzl",
"//python/private/common:py_binary_rule_bazel_bzl",
"//python/private/common:py_library_rule_bazel_bzl",
"//python/private/common:py_runtime_rule_bzl",
Expand Down
1 change: 1 addition & 0 deletions python/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ licenses(["notice"])
filegroup(
name = "distribution",
srcs = glob(["**"]) + [
"//python/api:distribution",
"//python/cc:distribution",
"//python/config_settings:distribution",
"//python/constraints:distribution",
Expand Down
27 changes: 27 additions & 0 deletions python/api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")

bzl_library(
name = "api_bzl",
srcs = ["api.bzl"],
visibility = ["//visibility:public"],
deps = ["//python/private/api:api_bzl"],
)

filegroup(
name = "distribution",
srcs = glob(["**"]),
)
5 changes: 5 additions & 0 deletions python/api/api.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Public, analysis phase APIs for Python rules."""

load("//python/private/api:api.bzl", _py_common = "py_common")

py_common = _py_common
43 changes: 43 additions & 0 deletions python/private/api/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load("@bazel_skylib//:bzl_library.bzl", "bzl_library")
load(":py_common_api.bzl", "py_common_api")

package(
default_visibility = ["//:__subpackages__"],
)

py_common_api(
name = "py_common_api",
# NOTE: Not actually public. Implicit dependency of public rules.
visibility = ["//visibility:public"],
)

bzl_library(
name = "api_bzl",
srcs = ["api.bzl"],
deps = [
"//python/private:py_info_bzl",
],
)

bzl_library(
name = "py_common_api_bzl",
srcs = ["py_common_api.bzl"],
deps = [
":api_bzl",
"//python/private:py_info_bzl",
],
)
55 changes: 55 additions & 0 deletions python/private/api/api.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Implementation of py_api."""

_PY_COMMON_API_LABEL = Label("//python/private/api:py_common_api")

ApiImplInfo = provider(
doc = "Provider to hold an API implementation",
fields = {
"impl": """
:type: struct
The implementation of the API being provided. The object it contains
will depend on the target that is providing the API struct.
""",
},
)

def _py_common_get(ctx):
"""Get the py_common API instance.
NOTE: to use this function, the rule must have added `py_common.API_ATTRS`
to its attributes.
Args:
ctx: {type}`ctx` current rule ctx
Returns:
{type}`PyCommonApi`
"""

# A generic provider is used to decouple the API implementations from
# the loading phase of the rules using an implementation.
return ctx.attr._py_common_api[ApiImplInfo].impl

py_common = struct(
get = _py_common_get,
API_ATTRS = {
"_py_common_api": attr.label(
default = _PY_COMMON_API_LABEL,
providers = [ApiImplInfo],
),
},
)
38 changes: 38 additions & 0 deletions python/private/api/py_common_api.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Implementation of py_api."""

load("//python/private:py_info.bzl", "PyInfoBuilder")
load("//python/private/api:api.bzl", "ApiImplInfo")

def _py_common_api_impl(ctx):
_ = ctx # @unused
return [ApiImplInfo(impl = PyCommonApi)]

py_common_api = rule(
implementation = _py_common_api_impl,
doc = "Rule implementing py_common API.",
)

def _merge_py_infos(transitive, *, direct = []):
builder = PyInfoBuilder()
builder.merge_all(transitive, direct = direct)
return builder.build()

# Exposed for doc generation, not directly used.
# buildifier: disable=name-conventions
PyCommonApi = struct(
merge_py_infos = _merge_py_infos,
PyInfoBuilder = PyInfoBuilder,
)
11 changes: 7 additions & 4 deletions python/private/py_info.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,14 @@ def _PyInfoBuilder_set_uses_shared_libraries(self, value):
self._uses_shared_libraries[0] = value
return self

def _PyInfoBuilder_merge(self, *infos):
return self.merge_all(infos)
def _PyInfoBuilder_merge(self, *infos, direct = []):
return self.merge_all(list(infos), direct = direct)

def _PyInfoBuilder_merge_all(self, py_infos):
for info in py_infos:
def _PyInfoBuilder_merge_all(self, transitive, *, direct = []):
for info in direct:
self.direct_pyc_files.add(info.direct_pyc_files)

for info in direct + transitive:
self.imports.add(info.imports)
self.merge_has_py2_only_sources(info.has_py2_only_sources)
self.merge_has_py3_only_sources(info.has_py3_only_sources)
Expand Down
17 changes: 17 additions & 0 deletions tests/api/py_common/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

load(":py_common_tests.bzl", "py_common_test_suite")

py_common_test_suite(name = "py_common_tests")
64 changes: 64 additions & 0 deletions tests/api/py_common/py_common_tests.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2024 The Bazel Authors. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""py_common tests."""

load("@rules_testing//lib:analysis_test.bzl", "analysis_test")
load("@rules_testing//lib:test_suite.bzl", "test_suite")
load("@rules_testing//lib:util.bzl", rt_util = "util")
load("//python/api:api.bzl", _py_common = "py_common")
load("//tests/support:py_info_subject.bzl", "py_info_subject")

_tests = []

def _test_merge_py_infos(name):
rt_util.helper_target(
native.filegroup,
name = name + "_subject",
srcs = ["f1.py", "f1.pyc", "f2.py", "f2.pyc"],
)
analysis_test(
name = name,
impl = _test_merge_py_infos_impl,
target = name + "_subject",
attrs = _py_common.API_ATTRS,
)

def _test_merge_py_infos_impl(env, target):
f1_py, f1_pyc, f2_py, f2_pyc = target[DefaultInfo].files.to_list()

py_common = _py_common.get(env.ctx)

py1 = py_common.PyInfoBuilder()
py1.direct_pyc_files.add(f1_pyc)
py1.transitive_sources.add(f1_py)

py2 = py_common.PyInfoBuilder()
py1.direct_pyc_files.add(f2_pyc)
py2.transitive_sources.add(f2_py)

actual = py_info_subject(
py_common.merge_py_infos([py2.build()], direct = [py1.build()]),
meta = env.expect.meta,
)

actual.transitive_sources().contains_exactly([f1_py.path, f2_py.path])
actual.direct_pyc_files().contains_exactly([f1_pyc.path, f2_pyc.path])

_tests.append(_test_merge_py_infos)

def py_common_test_suite(name):
test_suite(
name = name,
tests = _tests,
)
50 changes: 31 additions & 19 deletions tests/base_rules/py_info/py_info_tests.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -111,30 +111,25 @@ def _test_py_info_builder(name):
name = name + "_misc",
srcs = ["trans.py", "direct.pyc", "trans.pyc"],
)
rt_util.helper_target(
provide_py_info,
name = name + "_py1",
transitive_sources = ["py1-trans.py"],
direct_pyc_files = ["py1-direct-pyc.pyc"],
imports = ["py1import"],
transitive_pyc_files = ["py1-trans.pyc"],
)
rt_util.helper_target(
provide_py_info,
name = name + "_py2",
transitive_sources = ["py2-trans.py"],
direct_pyc_files = ["py2-direct.pyc"],
imports = ["py2import"],
transitive_pyc_files = ["py2-trans.pyc"],
)

py_info_targets = {}
for n in range(1, 7):
py_info_name = "{}_py{}".format(name, n)
py_info_targets["py{}".format(n)] = py_info_name
rt_util.helper_target(
provide_py_info,
name = py_info_name,
transitive_sources = ["py{}-trans.py".format(n)],
direct_pyc_files = ["py{}-direct.pyc".format(n)],
imports = ["py{}import".format(n)],
transitive_pyc_files = ["py{}-trans.pyc".format(n)],
)
analysis_test(
name = name,
impl = _test_py_info_builder_impl,
targets = {
"misc": name + "_misc",
"py1": name + "_py1",
"py2": name + "_py2",
},
} | py_info_targets,
)

def _test_py_info_builder_impl(env, targets):
Expand All @@ -151,6 +146,9 @@ def _test_py_info_builder_impl(env, targets):
builder.merge_target(targets.py1)
builder.merge_targets([targets.py2])

builder.merge(targets.py3[PyInfo], direct = [targets.py4[PyInfo]])
builder.merge_all([targets.py5[PyInfo]], direct = [targets.py6[PyInfo]])

def check(actual):
subject = py_info_subject(actual, meta = env.expect.meta)

Expand All @@ -162,20 +160,34 @@ def _test_py_info_builder_impl(env, targets):
"tests/base_rules/py_info/trans.py",
"tests/base_rules/py_info/py1-trans.py",
"tests/base_rules/py_info/py2-trans.py",
"tests/base_rules/py_info/py3-trans.py",
"tests/base_rules/py_info/py4-trans.py",
"tests/base_rules/py_info/py5-trans.py",
"tests/base_rules/py_info/py6-trans.py",
])
subject.imports().contains_exactly([
"import-path",
"py1import",
"py2import",
"py3import",
"py4import",
"py5import",
"py6import",
])
if hasattr(actual, "direct_pyc_files"):
subject.direct_pyc_files().contains_exactly([
"tests/base_rules/py_info/direct.pyc",
"tests/base_rules/py_info/py4-direct.pyc",
"tests/base_rules/py_info/py6-direct.pyc",
])
subject.transitive_pyc_files().contains_exactly([
"tests/base_rules/py_info/trans.pyc",
"tests/base_rules/py_info/py1-trans.pyc",
"tests/base_rules/py_info/py2-trans.pyc",
"tests/base_rules/py_info/py3-trans.pyc",
"tests/base_rules/py_info/py4-trans.pyc",
"tests/base_rules/py_info/py5-trans.pyc",
"tests/base_rules/py_info/py6-trans.pyc",
])

check(builder.build())
Expand Down

0 comments on commit 8a66b6e

Please sign in to comment.