Ejemplo n.º 1
0
def test_options_bool_flags():
    # Most options are default False.
    # New options should follow the dash-case/snake_case convention.
    opt_str_to_attr_name = {
        name: re.sub(r"-", "_", name)
        for name in
        ["lazy-import",
         "old-naming",
         "add-iam-methods",
         "metadata",
         "warehouse-package-name",
         ]}

    for opt, attr in opt_str_to_attr_name.items():
        options = Options.build("")
        assert not getattr(options, attr)

        options = Options.build(opt)
        assert getattr(options, attr)

    # Check autogen-snippets separately, as it is default True
    options = Options.build("")
    assert options.autogen_snippets

    options = Options.build("autogen-snippets=False")
    assert not options.autogen_snippets
Ejemplo n.º 2
0
def test_options_service_yaml_config(fs):
    opts = Options.build("")
    assert opts.service_yaml_config == {}

    service_yaml_fpath = "testapi_v1.yaml"
    fs.create_file(service_yaml_fpath,
                   contents=("type: google.api.Service\n"
                             "config_version: 3\n"
                             "name: testapi.googleapis.com\n"))
    opt_string = f"service-yaml={service_yaml_fpath}"
    opts = Options.build(opt_string)
    expected_config = {
        "config_version": 3,
        "name": "testapi.googleapis.com"
    }
    assert opts.service_yaml_config == expected_config

    service_yaml_fpath = "testapi_v2.yaml"
    fs.create_file(service_yaml_fpath,
                   contents=("config_version: 3\n"
                             "name: testapi.googleapis.com\n"))
    opt_string = f"service-yaml={service_yaml_fpath}"
    opts = Options.build(opt_string)
    expected_config = {
        "config_version": 3,
        "name": "testapi.googleapis.com"
    }
    assert opts.service_yaml_config == expected_config
Ejemplo n.º 3
0
def test_options_service_config(fs):
    opts = Options.build("")
    assert opts.retry is None

    # Default of None is okay, verify build can read a config.
    service_config_fpath = "service_config.json"
    fs.create_file(service_config_fpath, contents="""{
    "methodConfig": [
        {
            "name": [
                {
                  "service": "animalia.mollusca.v1beta1.Cephalopod",
                  "method": "IdentifySquid"
                }
            ],
            "retryPolicy": {
                "maxAttempts": 5,
                "maxBackoff": "3s",
                "initialBackoff": "0.2s",
                "backoffMultiplier": 2,
                "retryableStatusCodes": [
                    "UNAVAILABLE",
                    "UNKNOWN"
                ]
            },
            "timeout": "5s"
        }
      ]
    }""")

    opt_string = f"retry-config={service_config_fpath}"
    opts = Options.build(opt_string)

    # Verify the config was read in correctly.
    expected_cfg = {
        "methodConfig": [
            {
                "name": [
                    {
                        "service": "animalia.mollusca.v1beta1.Cephalopod",
                        "method": "IdentifySquid",
                    }
                ],
                "retryPolicy": {
                    "maxAttempts": 5,
                    "maxBackoff": "3s",
                    "initialBackoff": "0.2s",
                    "backoffMultiplier": 2,
                    "retryableStatusCodes":
                    [
                        "UNAVAILABLE",
                        "UNKNOWN"
                    ]
                },
                "timeout":"5s"
            }
        ]
    }
    assert opts.retry == expected_cfg
Ejemplo n.º 4
0
def test_options_autogen_snippets_false_for_old_naming():
    # NOTE: Snippets are not currently correct for the alternative (Ads) templates
    # so always disable snippetgen in that case
    # https://github.com/googleapis/gapic-generator-python/issues/1052
    options = Options.build("old-naming")
    assert not options.autogen_snippets

    # Even if autogen-snippets is set to True, do not enable snippetgen
    options = Options.build("old-naming,autogen-snippets=True")
    assert not options.autogen_snippets
def test_parse_sample_paths(fs):
    fpath = "sampledir/sample.yaml"
    fs.create_file(
        fpath,
        contents=dedent("""
            ---
            type: com.google.api.codegen.samplegen.v1p2.SampleConfigProto
            schema_version: 1.2.0
            samples:
            - service: google.cloud.language.v1.LanguageService
            """),
    )

    with pytest.raises(types.InvalidConfig):
        Options.build("samples=sampledir/,")
def test_build_factory():
    proto = descriptor_pb2.FileDescriptorProto(
        package='google.mollusc.v1alpha1'
    )
    old = naming.Naming.build(
        proto,
        opts=Options(old_naming=True)
    )
    assert old.versioned_module_name == 'mollusc.v1alpha1'

    new = naming.Naming.build(
        proto,
        opts=Options()
    )
    assert new.versioned_module_name == 'mollusc_v1alpha1'
Ejemplo n.º 7
0
    def build(
        cls,
        file_descriptor: descriptor_pb2.FileDescriptorProto,
        file_to_generate: bool,
        naming: api_naming.Naming,
        opts: Options = Options(),
        prior_protos: Mapping[str, 'Proto'] = None,
        load_services: bool = True,
        all_resources: Optional[Mapping[str, wrappers.MessageType]] = None,
    ) -> 'Proto':
        """Build and return a Proto instance.

        Args:
            file_descriptor (~.FileDescriptorProto): The protocol buffer
                object describing the proto file.
            file_to_generate (bool): Whether this is a file which is
                to be directly generated, or a dependency.
            naming (~.Naming): The :class:`~.Naming` instance associated
                with the API.
            prior_protos (~.Proto): Previous, already processed protos.
                These are needed to look up messages in imported protos.
            load_services (bool): Toggle whether the proto file should
                load its services. Not doing so enables a two-pass fix for
                LRO response and metadata types in certain situations.
        """
        return _ProtoBuilder(
            file_descriptor,
            file_to_generate=file_to_generate,
            naming=naming,
            opts=opts,
            prior_protos=prior_protos or {},
            load_services=load_services,
            all_resources=all_resources or {},
        ).proto
Ejemplo n.º 8
0
def generate(request: typing.BinaryIO, output: typing.BinaryIO) -> None:
    """Generate a full API client description."""
    # Load the protobuf CodeGeneratorRequest.
    req = plugin_pb2.CodeGeneratorRequest.FromString(request.read())

    # Pull apart arguments in the request.
    opts = Options.build(req.parameter)

    # Determine the appropriate package.
    # This generator uses a slightly different mechanism for determining
    # which files to generate; it tracks at package level rather than file
    # level.
    package = os.path.commonprefix([
        p.package for p in req.proto_file if p.name in req.file_to_generate
    ]).rstrip('.')

    # Build the API model object.
    # This object is a frozen representation of the whole API, and is sent
    # to each template in the rendering step.
    api_schema = api.API.build(req.proto_file, opts=opts, package=package)

    # Translate into a protobuf CodeGeneratorResponse; this reads the
    # individual templates and renders them.
    # If there are issues, error out appropriately.
    res = generator.Generator(opts).get_response(api_schema, opts)

    # Output the serialized response.
    output.write(res.SerializeToString())
def test_custom_template_directory():
    # Create a generator.
    opts = Options.build("python-gapic-templates=/templates/")
    g = generator.Generator(opts)

    # Assert that the Jinja loader will pull from the correct location.
    assert g._env.loader.searchpath == ["/templates"]
def test_get_response_enumerates_services():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/%service/baz.py.j2",
            "molluscs/squid/sample.py.j2",
        ]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("Service: {{ service.name }}")
            cgr = g.get_response(
                api_schema=make_api(
                    make_proto(
                        descriptor_pb2.FileDescriptorProto(service=[
                            descriptor_pb2.ServiceDescriptorProto(name="Spam"),
                            descriptor_pb2.ServiceDescriptorProto(
                                name="EggsService"),
                        ]), )),
                opts=Options.build(""),
            )
            assert len(cgr.file) == 2
            assert {i.name
                    for i in cgr.file} == {
                        "foo/spam/baz.py",
                        "foo/eggs_service/baz.py",
                    }
def test_generator_duplicate_samples(fs):
    config_fpath = "samples.yaml"
    fs.create_file(
        config_fpath,
        contents=dedent("""
            # Note: the samples are duplicates.
            type: com.google.api.codegen.samplegen.v1p2.SampleConfigProto
            schema_version: 1.2.0
            samples:
            - id: squid_sample
              region_tag: humboldt_tag
              rpc: get_squid
            - id: squid_sample
              region_tag: humboldt_tag
              rpc: get_squid
            """),
    )

    generator = make_generator("samples=samples.yaml")
    generator._env.loader = jinja2.DictLoader({"sample.py.j2": ""})
    api_schema = make_api(
        naming=naming.NewNaming(name="Mollusc", version="v6"))

    with pytest.raises(types.DuplicateSample):
        generator.get_response(api_schema=api_schema, opts=Options.build(""))
def test_get_response_ignores_unwanted_transports_and_clients():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/%service/transports/river.py.j2",
            "foo/%service/transports/car.py.j2",
            "foo/%service/transports/grpc.py.j2",
            "foo/%service/transports/__init__.py.j2",
            "foo/%service/transports/base.py.j2",
            "foo/%service/async_client.py.j2",
            "foo/%service/client.py.j2",
            "mollusks/squid/sample.py.j2",
        ]

        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("Service: {{ service.name }}")
            api_schema = make_api(
                make_proto(
                    descriptor_pb2.FileDescriptorProto(service=[
                        descriptor_pb2.ServiceDescriptorProto(
                            name="SomeService"),
                    ]), ))

            cgr = g.get_response(api_schema=api_schema,
                                 opts=Options.build("transport=river+car"))
            assert len(cgr.file) == 5
            assert {i.name
                    for i in cgr.file} == {
                        "foo/some_service/transports/river.py",
                        "foo/some_service/transports/car.py",
                        "foo/some_service/transports/__init__.py",
                        "foo/some_service/transports/base.py",
                        # Only generate async client with grpc transport
                        "foo/some_service/client.py",
                    }

            cgr = g.get_response(api_schema=api_schema,
                                 opts=Options.build("transport=grpc"))
            assert len(cgr.file) == 5
            assert {i.name
                    for i in cgr.file} == {
                        "foo/some_service/transports/grpc.py",
                        "foo/some_service/transports/__init__.py",
                        "foo/some_service/transports/base.py",
                        "foo/some_service/client.py",
                        "foo/some_service/async_client.py",
                    }
def test_cli_override_warehouse_package_name():
    FileDesc = descriptor_pb2.FileDescriptorProto
    proto1 = FileDesc(package='google.translation')
    n = naming.Naming.build(
        proto1,
        opts=Options(warehouse_package_name='google-cloud-foo'),
    )
    assert n.warehouse_package_name == "google-cloud-foo"
def test_cli_override_name_underscores():
    FileDesc = descriptor_pb2.FileDescriptorProto
    proto1 = FileDesc(package='google.cloud.videointelligence.v1')
    n = naming.Naming.build(proto1,
                            opts=Options(name='video_intelligence'),
                            )
    assert n.namespace == ('Google', 'Cloud')
    assert n.name == 'Video Intelligence'
    assert n.version == 'v1'
def test_cli_override_namespace_dotted():
    FileDesc = descriptor_pb2.FileDescriptorProto
    proto1 = FileDesc(package='google.spanner.v1')
    n = naming.Naming.build(proto1,
                            opts=Options(namespace=('google.cloud',)),
                            )
    assert n.namespace == ('Google', 'Cloud')
    assert n.name == 'Spanner'
    assert n.version == 'v1'
def test_options_trim_whitespace():
    # When writing shell scripts, users may construct options strings with
    # whitespace that needs to be trimmed after tokenizing.
    opts = Options.build('''
        python-gapic-templates=/squid/clam/whelk ,
        python-gapic-name=mollusca ,
        ''')
    assert opts.templates[0] == '/squid/clam/whelk'
    assert opts.name == 'mollusca'
def test_cli_override_name_and_namespace_versionless():
    FileDesc = descriptor_pb2.FileDescriptorProto
    proto1 = FileDesc(package='google.translation')
    n = naming.Naming.build(
        proto1,
        opts=Options(namespace=('google', 'cloud'), name='translate'),
    )
    assert n.namespace == ('Google', 'Cloud')
    assert n.name == 'Translate'
    assert not n.version
def test_get_response_fails_invalid_file_paths():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/bar/%service/%proto/baz.py.j2",
        ]
        with pytest.raises(ValueError) as ex:
            g.get_response(api_schema=make_api(), opts=Options.build(""))

        ex_str = str(ex.value)
        assert "%proto" in ex_str and "%service" in ex_str
def test_options_bool_flags():
    # All these options are default False.
    # If new options violate this assumption,
    # this test may need to be tweaked.
    # New options should follow the dash-case/snake_case convention.
    opt_str_to_attr_name = {
        name: re.sub(r"-", "_", name)
        for name in [
            "lazy-import",
            "old-naming",
            "add-iam-methods",
            "metadata",
            "warehouse-package-name",
        ]
    }

    for opt, attr in opt_str_to_attr_name.items():
        options = Options.build("")
        assert not getattr(options, attr)

        options = Options.build(opt)
        assert getattr(options, attr)
def test_get_response_ignores_empty_files():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = ["foo/bar/baz.py.j2", "molluscs/squid/sample.py.j2"]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("# Meaningless comment")
            cgr = g.get_response(api_schema=make_api(), opts=Options.build(""))
            lt.assert_called_once()
            gt.assert_has_calls([
                mock.call("foo/bar/baz.py.j2"),
                mock.call("molluscs/squid/sample.py.j2"),
            ])
            assert len(cgr.file) == 0
def test_get_response_ignore_gapic_metadata():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = ["gapic/gapic_metadata.json.j2"]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template(
                "This is not something we want to see")
            res = g.get_response(
                api_schema=make_api(),
                opts=Options.build(""),
            )

            # We don't expect any files because opts.metadata is not set.
            assert res.file == CodeGeneratorResponse().file
def test_generate_autogen_samples(mock_generate_sample, mock_generate_specs):
    opts = Options.build("autogen-snippets")
    g = generator.Generator(opts)
    # Need to have the sample template visible to the generator.
    g._env.loader = jinja2.DictLoader({"sample.py.j2": ""})

    api_schema = make_api(
        naming=naming.NewNaming(name="Mollusc", version="v6"))

    actual_response = g.get_response(api_schema, opts=opts)

    # Just check that generate_sample_specs was called
    # Correctness of the spec is tested in samplegen unit tests
    mock_generate_specs.assert_called_once_with(api_schema, opts=opts)
def test_get_response_divides_subpackages():
    # NOTE: autogen-snippets is intentionally disabled for this test
    # The API schema below is incomplete and will result in errors when the
    # snippetgen logic tries to parse it.
    g = make_generator("autogen-snippets=false")
    api_schema = api.API.build(
        [
            descriptor_pb2.FileDescriptorProto(
                name="top.proto",
                package="foo.v1",
                service=[descriptor_pb2.ServiceDescriptorProto(name="Top")],
            ),
            descriptor_pb2.FileDescriptorProto(
                name="a/spam/ham.proto",
                package="foo.v1.spam",
                service=[descriptor_pb2.ServiceDescriptorProto(name="Bacon")],
            ),
            descriptor_pb2.FileDescriptorProto(
                name="a/eggs/yolk.proto",
                package="foo.v1.eggs",
                service=[
                    descriptor_pb2.ServiceDescriptorProto(name="Scramble")
                ],
            ),
        ],
        package="foo.v1",
    )
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/%sub/types/%proto.py.j2",
            "foo/%sub/services/%service.py.j2",
            "molluscs/squid/sample.py.j2",
        ]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("""
                {{- '' }}Subpackage: {{ '.'.join(api.subpackage_view) }}
            """.strip())
            cgr = g.get_response(api_schema=api_schema,
                                 opts=Options.build("autogen-snippets=false"))
            assert len(cgr.file) == 6
            assert {i.name
                    for i in cgr.file} == {
                        "foo/types/top.py",
                        "foo/services/top.py",
                        "foo/spam/types/ham.py",
                        "foo/spam/services/bacon.py",
                        "foo/eggs/types/yolk.py",
                        "foo/eggs/services/scramble.py",
                    }
def test_get_response():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = ["foo/bar/baz.py.j2", "molluscs/squid/sample.py.j2"]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("I am a template result.")
            cgr = g.get_response(api_schema=make_api(), opts=Options.build(""))
            lt.assert_called_once()
            gt.assert_has_calls([
                mock.call("foo/bar/baz.py.j2"),
                mock.call("molluscs/squid/sample.py.j2"),
            ])
            assert len(cgr.file) == 1
            assert cgr.file[0].name == "foo/bar/baz.py"
            assert cgr.file[0].content == "I am a template result.\n"
Ejemplo n.º 25
0
def test_get_response_divides_subpackages():
    g = make_generator()
    api_schema = api.API.build(
        [
            descriptor_pb2.FileDescriptorProto(
                name="top.proto",
                package="foo.v1",
                service=[descriptor_pb2.ServiceDescriptorProto(name="Top")],
            ),
            descriptor_pb2.FileDescriptorProto(
                name="a/spam/ham.proto",
                package="foo.v1.spam",
                service=[descriptor_pb2.ServiceDescriptorProto(name="Bacon")],
            ),
            descriptor_pb2.FileDescriptorProto(
                name="a/eggs/yolk.proto",
                package="foo.v1.eggs",
                service=[descriptor_pb2.ServiceDescriptorProto(
                    name="Scramble")],
            ),
        ],
        package="foo.v1",
    )
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/%sub/types/%proto.py.j2",
            "foo/%sub/services/%service.py.j2",
            "molluscs/squid/sample.py.j2",
        ]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template(
                """
                {{- '' }}Subpackage: {{ '.'.join(api.subpackage_view) }}
            """.strip()
            )
            cgr = g.get_response(api_schema=api_schema,
                                 opts=Options.build(""))
            assert len(cgr.file) == 6
            assert {i.name for i in cgr.file} == {
                "foo/types/top.py",
                "foo/services/top.py",
                "foo/spam/types/ham.py",
                "foo/spam/services/bacon.py",
                "foo/eggs/types/yolk.py",
                "foo/eggs/services/scramble.py",
            }
def test_get_response_enumerates_proto():
    g = make_generator()
    with mock.patch.object(jinja2.FileSystemLoader, "list_templates") as lt:
        lt.return_value = [
            "foo/%proto.py.j2",
            "molluscs/squid/sample.py.j2",
        ]
        with mock.patch.object(jinja2.Environment, "get_template") as gt:
            gt.return_value = jinja2.Template("Proto: {{ proto.module_name }}")
            cgr = g.get_response(
                api_schema=make_api(
                    make_proto(
                        descriptor_pb2.FileDescriptorProto(name="a.proto")),
                    make_proto(
                        descriptor_pb2.FileDescriptorProto(name="b.proto")),
                ),
                opts=Options.build(""),
            )
            assert len(cgr.file) == 2
            assert {i.name for i in cgr.file} == {"foo/a.py", "foo/b.py"}
def test_samplegen_id_disambiguation(mock_gmtime, mock_generate_sample, fs):
    # These time values are nothing special,
    # they just need to be deterministic.
    returner = mock.MagicMock()
    returner.tm_year = 2112
    returner.tm_mon = 6
    returner.tm_mday = 1
    returner.tm_hour = 13
    returner.tm_min = 13
    returner.tm_sec = 13
    mock_gmtime.return_value = returner

    # Note: The first two samples will have the same nominal ID, the first by
    #       explicit naming and the second by falling back to the region_tag.
    #       The third has no id of any kind, so the generator is required to make a
    #       unique ID for it.
    fs.create_file(
        "samples.yaml",
        contents=dedent("""
            ---
            type: com.google.api.codegen.samplegen.v1p2.SampleConfigProto
            schema_version: 1.2.0
            samples:
            - id: squid_sample
              region_tag: humboldt_tag
              rpc: get_squid_streaming
            # Note that this region tag collides with the id of the previous sample.
            - region_tag: squid_sample
              rpc: get_squid_streaming
            # No id or region tag.
            - rpc: get_squid_streaming
            """),
    )
    g = generator.Generator(Options.build("samples=samples.yaml"))
    # Need to have the sample template visible to the generator.
    g._env.loader = jinja2.DictLoader({"sample.py.j2": ""})

    api_schema = make_api(
        naming=naming.NewNaming(name="Mollusc", version="v6"))
    actual_response = g.get_response(api_schema, opts=Options.build(""))
    expected_response = CodeGeneratorResponse(file=[
        CodeGeneratorResponse.File(
            name="samples/generated_samples/squid_sample_91a465c6.py",
            content="\n",
        ),
        CodeGeneratorResponse.File(
            name="samples/generated_samples/squid_sample_55051b38.py",
            content="\n",
        ),
        CodeGeneratorResponse.File(
            name="samples/generated_samples/157884ee.py",
            content="\n",
        ),
        # TODO(busunkim): Re-enable manifest generation once metadata
        # format has been formalized.
        # https://docs.google.com/document/d/1ghBam8vMj3xdoe4xfXhzVcOAIwrkbTpkMLgKc9RPD9k/edit#heading=h.sakzausv6hue
        # CodeGeneratorResponse.File(
        #     name="samples/generated_samples/mollusc.v6.python.21120601.131313.manifest.yaml",
        #     content=dedent(
        #         """\
        #     ---
        #     type: manifest/samples
        #     schema_version: 3
        #     python: &python
        #       environment: python
        #       bin: python3
        #       base_path: samples
        #       invocation: '{bin} {path} @args'
        #     samples:
        #     - <<: *python
        #       sample: squid_sample_91a465c6
        #       path: '{base_path}/squid_sample_91a465c6.py'
        #       region_tag: humboldt_tag
        #     - <<: *python
        #       sample: squid_sample_55051b38
        #       path: '{base_path}/squid_sample_55051b38.py'
        #       region_tag: squid_sample
        #     - <<: *python
        #       sample: 157884ee
        #       path: '{base_path}/157884ee.py'
        #     """
        #     ),
        # ),
    ])
    expected_response.supported_features |= (
        CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL)

    assert actual_response == expected_response
Ejemplo n.º 28
0
    def build(
            *file_descriptors: descriptor_pb2.FileDescriptorProto,
            opts: Options = Options(),
    ) -> 'Naming':
        """Return a full Naming instance based on these file descriptors.

        This is pieced together from the proto package names as well as the
        ``google.api.metadata`` file annotation. This information may be
        present in one or many files; this method is tolerant as long as
        the data does not conflict.

        Args:
            file_descriptors (Iterable[~.FileDescriptorProto]): A list of
                file descriptor protos. This list should only include the
                files actually targeted for output (not their imports).

        Returns:
            ~.Naming: A :class:`~.Naming` instance which is provided to
                templates as part of the :class:`~.API`.

        Raises:
            ValueError: If the provided file descriptors contain contradictory
                information.
        """
        # Determine the set of proto packages.
        proto_packages = {fd.package for fd in file_descriptors}
        root_package = os.path.commonprefix(tuple(proto_packages)).rstrip('.')

        # Sanity check: If there is no common ground in the package,
        # we are obviously in trouble.
        if not root_package:
            raise ValueError(
                'The protos provided do not share a common root package. '
                'Ensure that all explicitly-specified protos are for a '
                'single API. '
                f'The packages we got are: {", ".join(proto_packages)}')

        # Define the valid regex to split the package.
        #
        # It is not necessary for the regex to be as particular about package
        # name validity (e.g. avoiding .. or segments starting with numbers)
        # because protoc is guaranteed to give us valid package names.
        pattern = r'^((?P<namespace>[a-z0-9_.]+)\.)?(?P<name>[a-z0-9_]+)'

        # Only require the version portion of the regex if the version is
        # present.
        #
        # This code may look counter-intuitive (why not use ? to make it
        # optional), but the engine's greediness routine will decide that
        # the version is the name, which is not what we want.
        version = r'\.(?P<version>v[0-9]+(p[0-9]+)?((alpha|beta)[0-9]*)?)'
        if re.search(version, root_package):
            pattern += version

        # Okay, do the match
        match = cast(Match, re.search(pattern=pattern,
                                      string=root_package)).groupdict()
        match['namespace'] = match['namespace'] or ''
        klass = OldNaming if opts.old_naming else NewNaming
        package_info = klass(
            name=match['name'].capitalize(),
            namespace=tuple(i.capitalize()
                            for i in match['namespace'].split('.') if i),
            product_name=match['name'].capitalize(),
            proto_package=root_package,
            version=match.get('version', ''),
        )

        # Sanity check: Ensure that the package directives all inferred
        # the same information.
        if not package_info.version and len(proto_packages) > 1:
            raise ValueError('All protos must have the same proto package '
                             'up to and including the version.')

        # If a naming information was provided on the CLI, override the naming
        # value.
        #
        # We are liberal about what formats we take on the CLI; it will
        # likely make sense to many users to use dot-separated namespaces and
        # snake case, so handle that and do the right thing.
        if opts.name:
            package_info = dataclasses.replace(
                package_info,
                name=' '.join(
                    (i.capitalize()
                     for i in opts.name.replace('_', ' ').split(' '))))
        if opts.namespace:
            package_info = dataclasses.replace(
                package_info,
                namespace=tuple(
                    # The join-and-split on "." here causes us to expand out
                    # dot notation that we may have been sent; e.g. a one-tuple
                    # with ('x.y',) will become a two-tuple: ('x', 'y')
                    i.capitalize()
                    for i in '.'.join(opts.namespace).split('.')))

        # Done; return the naming information.
        return package_info
def make_generator(opts_str: str = "") -> generator.Generator:
    return generator.Generator(Options.build(opts_str))
def test_dont_generate_in_code_samples(mock_gmtime, mock_generate_sample, fs):
    # These time values are nothing special,
    # they just need to be deterministic.
    returner = mock.MagicMock()
    returner.tm_year = 2112
    returner.tm_mon = 6
    returner.tm_mday = 1
    returner.tm_hour = 13
    returner.tm_min = 13
    returner.tm_sec = 13
    mock_gmtime.return_value = returner

    config_fpath = "samples.yaml"
    fs.create_file(
        config_fpath,
        contents=dedent("""
            type: com.google.api.codegen.samplegen.v1p2.SampleConfigProto
            schema_version: 1.2.0
            samples:
            - id: squid_sample
              rpc: IdentifyMollusc
              service: Mollusc.v1.Mollusc
              sample_type:
              - standalone
              - incode/SQUID
            - id: clam_sample
              rpc: IdentifyMollusc
              service: Mollusc.v1.Mollusc
              sample_type:
              - incode/CLAM
            - id: whelk_sample
              rpc: IdentifyMollusc
              service: Mollusc.v1.Mollusc
              sample_type:
              - standalone
            - id: octopus_sample
              rpc: IdentifyMollusc
              service: Mollusc.v1.Mollusc
            """),
    )

    generator = make_generator(f"samples={config_fpath}")
    generator._env.loader = jinja2.DictLoader({"sample.py.j2": ""})
    api_schema = make_api(
        make_proto(
            descriptor_pb2.FileDescriptorProto(
                name="mollusc.proto",
                package="Mollusc.v1",
                service=[
                    descriptor_pb2.ServiceDescriptorProto(name="Mollusc")
                ],
            ), ),
        naming=naming.NewNaming(name="Mollusc", version="v6"),
    )

    # Note that we do NOT expect a clam sample.
    # There are four tests going on:
    # 1) Just an explicit standalone sample type.
    # 2) Multiple sample types, one of which is standalone.
    # 3) Explicit sample types but NO standalone sample type.
    # 4) Implicit standalone sample type.
    expected = CodeGeneratorResponse(file=[
        CodeGeneratorResponse.File(
            name="samples/generated_samples/squid_sample.py",
            content="\n",
        ),
        CodeGeneratorResponse.File(
            name="samples/generated_samples/whelk_sample.py",
            content="\n",
        ),
        CodeGeneratorResponse.File(
            name="samples/generated_samples/octopus_sample.py",
            content="\n",
        ),
        # TODO(busunkim): Re-enable manifest generation once metadata
        # format has been formalized.
        # https://docs.google.com/document/d/1ghBam8vMj3xdoe4xfXhzVcOAIwrkbTpkMLgKc9RPD9k/edit#heading=h.sakzausv6hue
        # CodeGeneratorResponse.File(
        #     name="samples/generated_samples/mollusc.v6.python.21120601.131313.manifest.yaml",
        #     content=dedent(
        #         """                ---
        #     type: manifest/samples
        #     schema_version: 3
        #     python: &python
        #       environment: python
        #       bin: python3
        #       base_path: samples
        #       invocation: \'{bin} {path} @args\'
        #     samples:
        #     - <<: *python
        #       sample: squid_sample
        #       path: \'{base_path}/squid_sample.py\'
        #     - <<: *python
        #       sample: whelk_sample
        #       path: \'{base_path}/whelk_sample.py\'
        #     - <<: *python
        #       sample: octopus_sample
        #       path: \'{base_path}/octopus_sample.py\'
        #     """
        #     ),
        # ),
    ])
    expected.supported_features |= CodeGeneratorResponse.Feature.FEATURE_PROTO3_OPTIONAL

    actual = generator.get_response(api_schema=api_schema,
                                    opts=Options.build(""))
    assert actual == expected