Repository: twisted/towncrier
Pull request #297 (2020-12-13 02:34:48)

Fix/revisit get_package

altendky: Just building on @blueyed's work in #176.  Maybe just by adding the test...
codecov-io: # [Codecov](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=h1) Report
> Merging [#297](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=desc) (30737a7) into [master](https://codecov.io/gh/twisted/towncrier/commit/ef1be451c44b71112cf6b6b2f1e90a27dd5ff489?el=desc) (ef1be45) will **decrease** coverage by `5.14%`.
> The diff coverage is `44.44%`.

[![Impacted file tree graph](https://codecov.io/gh/twisted/towncrier/pull/297/graphs/tree.svg?width=650&height=150&src=pr&token=9npndnog2E)](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #297      +/-   ##
==========================================
- Coverage   95.37%   90.23%   -5.15%     
==========================================
  Files          20       11       -9     
  Lines        1038      471     -567     
  Branches      104       94      -10     
==========================================
- Hits          990      425     -565     
+ Misses         27       25       -2     
  Partials       21       21              
```


| [Impacted Files](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/towncrier/\_project.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9fcHJvamVjdC5weQ==) | `58.97% <44.44%> (+2.21%)` | :arrow_up: |
| [src/towncrier/test/test\_write.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3Rfd3JpdGUucHk=) | | |
| [src/towncrier/test/test\_check.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfY2hlY2sucHk=) | | |
| [src/towncrier/test/test\_format.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfZm9ybWF0LnB5) | | |
| [src/towncrier/test/test\_build.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfYnVpbGQucHk=) | | |
| [src/towncrier/test/test\_settings.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3Rfc2V0dGluZ3MucHk=) | | |
| [src/towncrier/test/test\_builder.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfYnVpbGRlci5weQ==) | | |
| [src/towncrier/test/test\_create.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci90ZXN0L3Rlc3RfY3JlYXRlLnB5) | | |
| [src/towncrier/\_\_main\_\_.py](https://codecov.io/gh/twisted/towncrier/pull/297/diff?src=pr&el=tree#diff-c3JjL3Rvd25jcmllci9fX21haW5fXy5weQ==) | | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=footer). Last update [ef1be45...30737a7](https://codecov.io/gh/twisted/towncrier/pull/297?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

altendky: Am I correct that your comments about `get_version()` are effectively a suggestion for a new ticket?  Just making sure I understand before I go implementing the other suggestions.
adiroiban: > Am I correct that your comments about `get_version()` are effectively a suggestion for a new ticket?

Yes. Sorry for not being explicit. I was just venting by pain with Incremental :)


Patch:
diff --git a/src/towncrier/_project.py b/src/towncrier/_project.py
index 7b33798..1999eab 100644
--- a/src/towncrier/_project.py
+++ b/src/towncrier/_project.py
@@ -16,26 +16,22 @@ from incremental import Version
 
 def _get_package(package_dir, package):
 
-    # Step 1: Try the dumbest and simplest thing that could possibly work.
-    # Yes, that means importing it. Call the cops, I don't care.
-
-    sys.path.insert(1, package_dir)
-
     try:
         module = import_module(package)
-    except ImportError as e:
-        print("Tried to import {}, but ran into this error: {}".format(package, e))
-        # wups that didn't work
-        module = None
-
-    # Don't leave trash in sys.path
-    sys.path.pop(0)
-
-    # Step 2: uhhhhhhh
-    # TBA
-
-    if not module:
-        raise Exception("Can't find your project :(")
+    except ImportError:
+        # Package is not already available / installed.
+        # Force importing it based on the source files.
+        sys.path.insert(0, package_dir)
+
+        try:
+            module = import_module(package)
+        except ImportError as e:
+            err = "tried to import {}, but ran into this error: {}".format(package, e)
+            # NOTE: this might be redirected via "towncrier --draft > …".
+            print("ERROR: {}".format(err))
+            raise
+        finally:
+            sys.path.pop(0)
 
     return module
 
diff --git a/src/towncrier/newsfragments/297.feature.rst b/src/towncrier/newsfragments/297.feature.rst
new file mode 100644
index 0000000..f70d159
--- /dev/null
+++ b/src/towncrier/newsfragments/297.feature.rst
@@ -0,0 +1,2 @@
+When searching for the project, first check for an existing importable instance.
+This helps if the version is only available in the installed version and not the source.


Test patch:
diff --git a/src/towncrier/test/test_project.py b/src/towncrier/test/test_project.py
index a8930a2..01bdf0e 100644
--- a/src/towncrier/test/test_project.py
+++ b/src/towncrier/test/test_project.py
@@ -39,6 +39,66 @@ class VersionFetchingTests(TestCase):
         version = get_version(temp, "mytestproja")
         self.assertEqual(version, "1.3.12")
 
+    def test_import_fails(self):
+        """
+        An exception is raised when getting the version failed due to missing Python package files.
+        """
+        if sys.version_info >= (3, 6):
+            expected_exception = ModuleNotFoundError
+        else:
+            expected_exception = ImportError
+
+        with self.assertRaises(expected_exception):
+            get_version(".", "projectname_without_any_files")
+
+    def test_already_installed_import(self):
+        """
+        An already installed package will be checked before cwd-found packages.
+        """
+        project_name = "mytestproj_already_installed_import"
+
+        temp = self.mktemp()
+        os.makedirs(temp)
+        os.makedirs(os.path.join(temp, project_name))
+
+        with open(os.path.join(temp, project_name, "__init__.py"), "w") as f:
+            f.write("__version__ = (1, 3, 12)")
+
+        sys_path_temp = self.mktemp()
+        os.makedirs(sys_path_temp)
+        os.makedirs(os.path.join(sys_path_temp, project_name))
+
+        with open(os.path.join(sys_path_temp, project_name, "__init__.py"), "w") as f:
+            f.write("__version__ = (2, 1, 5)")
+
+        sys.path.insert(0, sys_path_temp)
+        self.addCleanup(sys.path.pop, 0)
+
+        version = get_version(temp, project_name)
+
+        self.assertEqual(version, "2.1.5")
+
+    def test_installed_package_found_when_no_source_present(self):
+        """
+        The version from the installed package is returned when there is no
+        package present at the provided source directory.
+        """
+        project_name = "mytestproj_only_installed"
+
+        sys_path_temp = self.mktemp()
+        os.makedirs(sys_path_temp)
+        os.makedirs(os.path.join(sys_path_temp, project_name))
+
+        with open(os.path.join(sys_path_temp, project_name, "__init__.py"), "w") as f:
+            f.write("__version__ = (3, 14)")
+
+        sys.path.insert(0, sys_path_temp)
+        self.addCleanup(sys.path.pop, 0)
+
+        version = get_version("some non-existent directory", project_name)
+
+        self.assertEqual(version, "3.14")
+
 
 class InvocationTests(TestCase):
     def test_dash_m(self):