diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py index 2268619b..74ad5044 100644 --- a/importlib_metadata/__init__.py +++ b/importlib_metadata/__init__.py @@ -51,6 +51,7 @@ 'SimplePath', 'distribution', 'distributions', + 'editable', 'entry_points', 'files', 'metadata', @@ -733,6 +734,18 @@ def url_req_space(req): def origin(self): return self._load_json('direct_url.json') + @property + def editable(self) -> bool: + """ + Whether the distribution is an editable install (:pep:`660`). + + Reads ``dir_info.editable`` from the :pep:`610` ``direct_url.json``, + defaulting to ``False`` when there is no origin or the package was + not installed from a local directory. + """ + dir_info = getattr(self.origin, 'dir_info', None) + return bool(getattr(dir_info, 'editable', False)) + def _load_json(self, filename): # Deferred for performance (python/importlib_metadata#503) import json @@ -1112,6 +1125,15 @@ def version(distribution_name: str) -> str: return distribution(distribution_name).version +def editable(distribution_name: str) -> bool: + """Return whether the named distribution is an editable install. + + :param distribution_name: The name of the distribution package to query. + :return: True if the package is installed in editable mode (:pep:`660`). + """ + return distribution(distribution_name).editable + + _unique = functools.partial( unique_everseen, key=operator.attrgetter('_normalized_name'), diff --git a/newsfragments/510.feature.rst b/newsfragments/510.feature.rst new file mode 100644 index 00000000..185959c5 --- /dev/null +++ b/newsfragments/510.feature.rst @@ -0,0 +1 @@ +Added ``Distribution.editable`` property and top-level ``editable()`` function reporting whether a distribution is an editable install (:pep:`660`). diff --git a/tests/fixtures.py b/tests/fixtures.py index 0416e4a4..7e09d653 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -126,6 +126,21 @@ class DistInfoPkgEditable(DistInfoPkg): } +class DistInfoPkgEditableDirInfo(DistInfoPkg): + """ + Package installed as editable from a local directory (:pep:`660`). + """ + + files: FilesSpec = { + 'distinfo_pkg-1.0.0.dist-info': { + 'direct_url.json': json.dumps({ + "dir_info": {"editable": True}, + "url": "file:///path/to/distinfo_pkg", + }) + }, + } + + class DistInfoPkgWithDot(OnSysPath, SiteBuilder): files: FilesSpec = { "pkg_dot-1.0.0.dist-info": { diff --git a/tests/test_main.py b/tests/test_main.py index f306f3d9..f23b0154 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -489,3 +489,23 @@ def test_origin(self): dist = Distribution.from_name('distinfo-pkg') assert dist.origin.url.endswith('.whl') assert dist.origin.archive_info.hashes.sha256 + + def test_not_editable_from_archive(self): + # An origin without dir_info (here an archive) is not editable. + assert Distribution.from_name('distinfo-pkg').editable is False + assert importlib_metadata.editable('distinfo-pkg') is False + + +class EditableDirInfoDistributionTest( + fixtures.DistInfoPkgEditableDirInfo, unittest.TestCase +): + def test_editable(self): + assert Distribution.from_name('distinfo-pkg').editable is True + assert importlib_metadata.editable('distinfo-pkg') is True + + +class NonEditableDistributionTest(fixtures.DistInfoPkg, unittest.TestCase): + def test_not_editable_without_origin(self): + # No direct_url.json means no origin, so not editable. + assert Distribution.from_name('distinfo-pkg').editable is False + assert importlib_metadata.editable('distinfo-pkg') is False