Repository: kinow/protobuf-uml-diagram

Issue:
Having proto as a module name makes things tricky
When I run the program with this command:

```
protobuf-uml-diagram --output doc --proto gs.proto.configs_data_pb2
```

I get:

```
ModuleNotFoundError: No module named 'gs_pb2'
```

After some initial confusion, I found the reason:

```python
return import_module(proto.replace(".proto", "_pb2").replace("/", "."))
```

This innocent line which makes the application work with both module path and file path, doesn't handle mine module structure. Fortunately, because it was not `proto.replace("/", ".").replace(".proto", "_pb2")`, I was able to make it work with simply using `/` instead of `.`. But what would be a legit solution for this case? Having two separate click options for proto source file and proto python module?

Discussion:
codecov-commenter: # [Codecov](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita) Report
> Merging [#30](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita) (cb6100b) into [master](https://codecov.io/gh/kinow/protobuf-uml-diagram/commit/b126306f46147d912d770b5b10b67dd56f98ed40?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita) (b126306) will **decrease** coverage by `3.12%`.
> The diff coverage is `66.66%`.

> :exclamation: Current head cb6100b differs from pull request most recent head acf4dc6. Consider uploading reports for the commit acf4dc6 to get more accurate results
[![Impacted file tree graph](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30/graphs/tree.svg?width=650&height=150&src=pr&token=53coqa1fKL&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita)](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita)

```diff
@@             Coverage Diff             @@
##            master      #30      +/-   ##
===========================================
- Coverage   100.00%   96.87%   -3.13%     
===========================================
  Files            1        1              
  Lines           89       96       +7     
  Branches        13       14       +1     
===========================================
+ Hits            89       93       +4     
- Misses           0        3       +3     
```


| [Impacted Files](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita) | Coverage Δ | |
|---|---|---|
| [protobuf\_uml\_diagram.py](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita#diff-cHJvdG9idWZfdW1sX2RpYWdyYW0ucHk=) | `96.87% <66.66%> (-3.13%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita). Last update [b126306...acf4dc6](https://codecov.io/gh/kinow/protobuf-uml-diagram/pull/30?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Bruno+P.+Kinoshita).


Patch:
diff --git a/CHANGES.md b/CHANGES.md
index b064882..2bbb705 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -16,7 +16,9 @@
 - Update protobuf requirement from <3.17,>=3.13 to >=3.13,<3.18 [#25](https://github.com/kinow/protobuf-uml-diagram/pull/25)
 - Update pytest-cov requirement from <2.12,>=2.10 to >=2.10,<2.13 [#26](https://github.com/kinow/protobuf-uml-diagram/pull/26)
 - Update graphviz requirement from <0.17,>=0.14 to >=0.14,<0.18 [#28](https://github.com/kinow/protobuf-uml-diagram/pull/28)
--  Update protobuf requirement from <3.18,>=3.13 to >=3.13,<3.19 [#29](https://github.com/kinow/protobuf-uml-diagram/pull/29)
+- Update protobuf requirement from <3.18,>=3.13 to >=3.13,<3.19 [#29](https://github.com/kinow/protobuf-uml-diagram/pull/29)
+- Fix when a module contains .proto within its name, but doesn't necessarily end with .proto
+[#27](https://github.com/kinow/protobuf-uml-diagram/issues/27)
 
 ## 0.7 (06-10-2020)
 
diff --git a/protobuf_uml_diagram.py b/protobuf_uml_diagram.py
index c039731..b591d77 100644
--- a/protobuf_uml_diagram.py
+++ b/protobuf_uml_diagram.py
@@ -161,7 +161,11 @@ def _module(proto: str) -> ModuleType:
     :return: Protobuf compiled Python module
     :rtype: ModuleType
     """
-    return import_module(proto.replace(".proto", "_pb2").replace("/", "."))
+    if proto.endswith('.proto'):
+        no_extension = f'{proto[:-len(".proto")]}_pb2'
+    else:
+        no_extension = proto
+    return import_module(no_extension.replace("/", "."))
 
 
 # -- Diagram builder
@@ -176,7 +180,11 @@ class Diagram:
     def from_file(self, proto_file: str):
         if not proto_file:
             raise ValueError("Missing proto file!")
-        self._proto_module = _module(proto_file)
+        try:
+            self._proto_module = _module(proto_file)
+        except ModuleNotFoundError as e:
+            logger.error(f'Failed to import {proto_file}')
+            raise e
         logger.info(f"Imported: {proto_file}")
         return self
 


Test patch:
diff --git a/test_data/issue_27/proto/configs_data_pb2.py b/test_data/issue_27/proto/configs_data_pb2.py
new file mode 100644
index 0000000..c7d3488
--- /dev/null
+++ b/test_data/issue_27/proto/configs_data_pb2.py
@@ -0,0 +1,84 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler.  DO NOT EDIT!
+# source: issue_27.proto
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import message as _message
+from google.protobuf import reflection as _reflection
+from google.protobuf import symbol_database as _symbol_database
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor.FileDescriptor(
+  name='issue_27.proto',
+  package='proto_messages',
+  syntax='proto3',
+  serialized_options=None,
+  create_key=_descriptor._internal_create_key,
+  serialized_pb=b'\n\x0eissue_27.proto\x12\x0eproto_messages\"3\n\nHelloWorld\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\n\n\x02id\x18\x02 \x01(\r\x12\x0b\n\x03msg\x18\x03 \x01(\tb\x06proto3'
+)
+
+
+
+
+_HELLOWORLD = _descriptor.Descriptor(
+  name='HelloWorld',
+  full_name='proto_messages.HelloWorld',
+  filename=None,
+  file=DESCRIPTOR,
+  containing_type=None,
+  create_key=_descriptor._internal_create_key,
+  fields=[
+    _descriptor.FieldDescriptor(
+      name='name', full_name='proto_messages.HelloWorld.name', index=0,
+      number=1, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='id', full_name='proto_messages.HelloWorld.id', index=1,
+      number=2, type=13, cpp_type=3, label=1,
+      has_default_value=False, default_value=0,
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+    _descriptor.FieldDescriptor(
+      name='msg', full_name='proto_messages.HelloWorld.msg', index=2,
+      number=3, type=9, cpp_type=9, label=1,
+      has_default_value=False, default_value=b"".decode('utf-8'),
+      message_type=None, enum_type=None, containing_type=None,
+      is_extension=False, extension_scope=None,
+      serialized_options=None, file=DESCRIPTOR,  create_key=_descriptor._internal_create_key),
+  ],
+  extensions=[
+  ],
+  nested_types=[],
+  enum_types=[
+  ],
+  serialized_options=None,
+  is_extendable=False,
+  syntax='proto3',
+  extension_ranges=[],
+  oneofs=[
+  ],
+  serialized_start=34,
+  serialized_end=85,
+)
+
+DESCRIPTOR.message_types_by_name['HelloWorld'] = _HELLOWORLD
+_sym_db.RegisterFileDescriptor(DESCRIPTOR)
+
+HelloWorld = _reflection.GeneratedProtocolMessageType('HelloWorld', (_message.Message,), {
+  'DESCRIPTOR' : _HELLOWORLD,
+  '__module__' : 'issue_27_pb2'
+  # @@protoc_insertion_point(class_scope:proto_messages.HelloWorld)
+  })
+_sym_db.RegisterMessage(HelloWorld)
+
+
+# @@protoc_insertion_point(module_scope)
diff --git a/tests.py b/tests.py
index 949b8b0..1da1734 100644
--- a/tests.py
+++ b/tests.py
@@ -85,3 +85,21 @@ class TestDiagramBuilder:
                 .build()
             assert os.path.getsize(tf) > 0
 
+    def test_logs_module_not_found(self):
+        with pytest.raises(ModuleNotFoundError) as e:
+            Diagram() \
+                .from_file('piracicaba') \
+                .build()
+        assert 'piracicaba' in str(e)
+
+    def test_contains_dot_proto_in_middle_of_the_name(self):
+        """A test where the input data contains .proto, but doesn't end with it."""
+        with TemporaryDirectory() as tmpdir:
+            tf = os.path.join(tmpdir, 'diagram.png')
+            Diagram() \
+                .from_file('test_data.issue_27.proto.configs_data_pb2') \
+                .to_file(Path(tf)) \
+                .with_format('png') \
+                .build()
+            assert os.path.getsize(tf) > 0
+