| # Copyright 2019 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. | 
 |  | 
 | """Defines an aspect for finding constraints on the Python version.""" | 
 |  | 
 | _PY2 = "PY2" | 
 | _PY3 = "PY3" | 
 |  | 
 | _TransitiveVersionInfo = provider( | 
 |     doc = """\ | 
 | Propagates information about the Python version constraints of transitive | 
 | dependencies. | 
 |  | 
 | Canonically speaking, a target is considered to be PY2-only if it returns the | 
 | `py` provider with the `has_py2_only_sources` field set to `True`. Likewise, it | 
 | is PY3-only if `has_py3_only_sources` is `True`. Unless something weird is going | 
 | on with how the transitive sources are aggregated, it is expected that if any | 
 | target is PY2-only or PY3-only, then so are all of its reverse transitive deps. | 
 |  | 
 | The `py_library` rule becomes PY2-only or PY3-only when its `srcs_version` | 
 | attribute is respectively set to `PY2ONLY` or to either `PY3` or `PY3ONLY`. | 
 | (The asymmetry of not recongizing `PY2` is due to | 
 | [#1393](https://github.com/bazelbuild/bazel/issues/1393) and will be moot once | 
 | the `PY2ONLY` and `PY3ONLY` names are retired.) Therefore, if the transitive | 
 | deps of the root target are all `py_library` targets, we can look at the | 
 | `srcs_version` attribute to easily distinguish targets whose own sources | 
 | require a given Python version, from targets that only require it due to their | 
 | transitive deps. | 
 |  | 
 | If on the other hand there are other rule types in the transitive deps that do | 
 | not define `srcs_version`, then the only general way to tell that a dep | 
 | introduces a requirement on Python 2 or 3 is if it returns true in the | 
 | corresponding provider field and none of its direct dependencies returns true | 
 | in that field. | 
 |  | 
 | This `_TransitiveVersionInfo` provider reports transitive deps that satisfy | 
 | either of these criteria. But of those deps, it only reports those that are | 
 | "top-most" in relation to the root. The top-most deps are the ones that are | 
 | reachable from the root target by a path that does not involve any other | 
 | top-most dep (though it's possible for one top-most dep to have a separate path | 
 | to another). Reporting only the top-most deps ensures that we give the minimal | 
 | information needed to understand how the root target depends on PY2-only or | 
 | PY3-only targets. | 
 | """, | 
 |     fields = { | 
 |         "py2": """\ | 
 | A `_DepsWithPathsInfo` object for transitive deps that are known to introduce a | 
 | PY2-only requirement. | 
 | """, | 
 |         "py3": """\ | 
 | A `_DepsWithPathsInfo` object for transitive deps that are known to introduce a | 
 | PY3-only requirement. | 
 | """, | 
 |     }, | 
 | ) | 
 |  | 
 | _DepsWithPathsInfo = provider( | 
 |     fields = { | 
 |         "topmost": """\ | 
 | A list of labels of all top-most transitive deps known to introduce a version | 
 | requirement. The deps appear in left-to-right order. | 
 | """, | 
 |         "paths": """\ | 
 | A dictionary that maps labels appearing in `topmost` to their paths from the | 
 | root. Paths are represented as depsets with `preorder` order. | 
 | """, | 
 |         # It is technically possible for the depset keys to collide if the same | 
 |         # target appears multiple times in the build graph as different | 
 |         # configured targets, but this seems unlikely. | 
 |     }, | 
 | ) | 
 |  | 
 | def _join_lines(nodes): | 
 |     return "\n".join([str(n) for n in nodes]) if nodes else "<None>" | 
 |  | 
 | def _str_path(path): | 
 |     return " -> ".join([str(p) for p in path.to_list()]) | 
 |  | 
 | def _str_tv_info(tv_info): | 
 |     """Returns a string representation of a `_TransitiveVersionInfo`.""" | 
 |     path_lines = [] | 
 |     path_lines.extend([_str_path(tv_info.py2.paths[n]) for n in tv_info.py2.topmost]) | 
 |     path_lines.extend([_str_path(tv_info.py3.paths[n]) for n in tv_info.py3.topmost]) | 
 |     return """\ | 
 | Python 2-only deps: | 
 | {py2_nodes} | 
 |  | 
 | Python 3-only deps: | 
 | {py3_nodes} | 
 |  | 
 | Paths to these deps: | 
 | {paths} | 
 | """.format( | 
 |         py2_nodes = _join_lines(tv_info.py2.topmost), | 
 |         py3_nodes = _join_lines(tv_info.py3.topmost), | 
 |         paths = _join_lines(path_lines), | 
 |     ) | 
 |  | 
 | def _has_version_requirement(target, version): | 
 |     """Returns whether a target has a version requirement, as per its provider. | 
 |  | 
 |     Args: | 
 |         target: the `Target` object to check | 
 |         version: either the string "PY2" or "PY3" | 
 |  | 
 |     Returns: | 
 |         `True` if `target` requires `version` according to the | 
 |         `has_py<?>_only_sources` fields | 
 |     """ | 
 |     if version not in [_PY2, _PY3]: | 
 |         fail("Unrecognized version '%s'; must be 'PY2' or 'PY3'" % version) | 
 |     field = { | 
 |         _PY2: "has_py2_only_sources", | 
 |         _PY3: "has_py3_only_sources", | 
 |     }[version] | 
 |  | 
 |     if not PyInfo in target: | 
 |         return False | 
 |     field_value = getattr(target[PyInfo], field, False) | 
 |     if not type(field_value) == "bool": | 
 |         fail("Invalid type for provider field '%s': %r" % (field, field_value)) | 
 |     return field_value | 
 |  | 
 | def _introduces_version_requirement(target, target_attr, version): | 
 |     """Returns whether a target introduces a PY2-only or PY3-only requirement. | 
 |  | 
 |     A target that has a version requirement is considered to introduce this | 
 |     requirement if either 1) its rule type has a `srcs_version` attribute and | 
 |     the target sets it to `PY2ONLY` (PY2), or `PY3` or `PY3ONLY` (PY3); or 2) | 
 |     none of its direct dependencies set `has_py2_only_sources` (PY2) or | 
 |     `has_py3_only_sources` (PY3) to `True`. A target that does not actually have | 
 |     the version requirement is never considered to introduce the requirement. | 
 |  | 
 |     Args: | 
 |         target: the `Target` object as passed to the aspect implementation | 
 |             function | 
 |         target_attr: the attribute struct as retrieved from `ctx.rule.attr` in | 
 |             the aspect implementation function | 
 |         version: either the string "PY2" or "PY3" indicating which constraint | 
 |             to test for | 
 |  | 
 |     Returns: | 
 |         `True` if `target` introduces the requirement on `version`, as per the | 
 |         above definition | 
 |     """ | 
 |     if version not in [_PY2, _PY3]: | 
 |         fail("Unrecognized version '%s'; must be 'PY2' or 'PY3'" % version) | 
 |  | 
 |     # If we don't actually have the version requirement, we can't possibly | 
 |     # introduce it, regardless of our srcs_version or what our dependencies | 
 |     # return. | 
 |     if not _has_version_requirement(target, version): | 
 |         return False | 
 |  | 
 |     # Try the attribute, if present. | 
 |     if hasattr(target_attr, "srcs_version"): | 
 |         sv = target_attr.srcs_version | 
 |         if version == _PY2: | 
 |             if sv == "PY2ONLY": | 
 |                 return True | 
 |         elif version == _PY3: | 
 |             if sv in ["PY3", "PY3ONLY"]: | 
 |                 return True | 
 |         else: | 
 |             fail("Illegal state") | 
 |  | 
 |     # No good, check the direct deps' provider fields. | 
 |     if not hasattr(target_attr, "deps"): | 
 |         return True | 
 |     else: | 
 |         return not any([ | 
 |             _has_version_requirement(dep, version) | 
 |             for dep in target_attr.deps | 
 |         ]) | 
 |  | 
 | def _empty_depswithpaths(): | 
 |     """Initializes an empty `_DepsWithPathsInfo` object.""" | 
 |     return _DepsWithPathsInfo(topmost = [], paths = {}) | 
 |  | 
 | def _init_depswithpaths_for_node(node): | 
 |     """Initialize a new `_DepsWithPathsInfo` object. | 
 |  | 
 |     The object will record just the given node as its sole entry. | 
 |  | 
 |     Args: | 
 |         node: a label | 
 |  | 
 |     Returns: | 
 |         a `_DepsWithPathsInfo` object | 
 |     """ | 
 |     return _DepsWithPathsInfo( | 
 |         topmost = [node], | 
 |         paths = {node: depset(direct = [node], order = "preorder")}, | 
 |     ) | 
 |  | 
 | def _merge_depswithpaths_appending_node(depswithpaths, node_to_append): | 
 |     """Merge several `_DepsWithPathsInfo` objects and appends a path entry. | 
 |  | 
 |     Args: | 
 |         depswithpaths: a list of `_DepsWithPathsInfo` objects whose entries are | 
 |             to be merged | 
 |         node_to_append: a label to append to all the paths of the merged object | 
 |  | 
 |     Returns: | 
 |         a `_DepsWithPathsInfo` object | 
 |     """ | 
 |     seen = {} | 
 |     topmost = [] | 
 |     paths = {} | 
 |     for dwp in depswithpaths: | 
 |         for node in dwp.topmost: | 
 |             if node in seen: | 
 |                 continue | 
 |             seen[node] = True | 
 |  | 
 |             topmost.append(node) | 
 |             path = dwp.paths[node] | 
 |             path = depset( | 
 |                 direct = [node_to_append], | 
 |                 transitive = [path], | 
 |                 order = "preorder", | 
 |             ) | 
 |             paths[node] = path | 
 |     return _DepsWithPathsInfo(topmost = topmost, paths = paths) | 
 |  | 
 | def _find_requirements_impl(target, ctx): | 
 |     # Determine whether this target introduces a requirement. If so, any deps | 
 |     # that introduce that requirement are not propagated, though they might | 
 |     # still be considered top-most if an alternate path exists. | 
 |     if not hasattr(ctx.rule.attr, "deps"): | 
 |         dep_tv_infos = [] | 
 |     else: | 
 |         dep_tv_infos = [ | 
 |             d[_TransitiveVersionInfo] | 
 |             for d in ctx.rule.attr.deps | 
 |             if _TransitiveVersionInfo in d | 
 |         ] | 
 |  | 
 |     if not _has_version_requirement(target, "PY2"): | 
 |         new_py2 = _empty_depswithpaths() | 
 |     elif _introduces_version_requirement(target, ctx.rule.attr, "PY2"): | 
 |         new_py2 = _init_depswithpaths_for_node(target.label) | 
 |     else: | 
 |         new_py2 = _merge_depswithpaths_appending_node( | 
 |             [i.py2 for i in dep_tv_infos], | 
 |             target.label, | 
 |         ) | 
 |  | 
 |     if not _has_version_requirement(target, "PY3"): | 
 |         new_py3 = _empty_depswithpaths() | 
 |     elif _introduces_version_requirement(target, ctx.rule.attr, "PY3"): | 
 |         new_py3 = _init_depswithpaths_for_node(target.label) | 
 |     else: | 
 |         new_py3 = _merge_depswithpaths_appending_node( | 
 |             [i.py3 for i in dep_tv_infos], | 
 |             target.label, | 
 |         ) | 
 |  | 
 |     tv_info = _TransitiveVersionInfo(py2 = new_py2, py3 = new_py3) | 
 |  | 
 |     output = ctx.actions.declare_file(target.label.name + "-pyversioninfo.txt") | 
 |     ctx.actions.write(output = output, content = _str_tv_info(tv_info)) | 
 |  | 
 |     return [tv_info, OutputGroupInfo(pyversioninfo = depset(direct = [output]))] | 
 |  | 
 | find_requirements = aspect( | 
 |     implementation = _find_requirements_impl, | 
 |     attr_aspects = ["deps"], | 
 |     doc = """\ | 
 | The aspect definition. Can be invoked on the command line as | 
 |  | 
 |     bazel build //pkg:my_py_binary_target \ | 
 |         --aspects=@bazel_tools//tools/python:srcs_version.bzl%find_requirements \ | 
 |         --output_groups=pyversioninfo | 
 | """, | 
 | ) | 
 |  | 
 | def _apply_find_requirements_for_testing_impl(ctx): | 
 |     tv_info = ctx.attr.target[_TransitiveVersionInfo] | 
 |     ctx.actions.write(output = ctx.outputs.out, content = _str_tv_info(tv_info)) | 
 |  | 
 | apply_find_requirements_for_testing = rule( | 
 |     implementation = _apply_find_requirements_for_testing_impl, | 
 |     attrs = { | 
 |         "target": attr.label(aspects = [find_requirements]), | 
 |         "out": attr.output(), | 
 |     }, | 
 |     doc = """\ | 
 | Writes the string output of `find_requirements` to a file. | 
 |  | 
 | This helper exists for the benefit of PythonSrcsVersionAspectTest.java. It is | 
 | useful because code outside this file cannot read the private | 
 | `_TransitiveVersionInfo` provider, and `BuildViewTestCase` cannot easily access | 
 | actions generated by an aspect. | 
 | """, | 
 | ) |