From feacd65ebcc00a03bfabf68283970637a8bc72ab Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sun, 26 Nov 2023 16:10:07 -0500 Subject: [PATCH] Allow using an empty string for the `sources` option to add a prefix to distribution paths (#1064) * Allow using empty string to add prefixes to the distribution path Closes #802 * update example Co-Authored-By: Vincent Hatakeyama <6304302+vincent-hatakeyama@users.noreply.github.com> * add release note Co-Authored-By: Vincent Hatakeyama <6304302+vincent-hatakeyama@users.noreply.github.com> * fix lint Co-Authored-By: Vincent Hatakeyama <6304302+vincent-hatakeyama@users.noreply.github.com> --------- Co-authored-by: Vincent Hatakeyama Co-authored-by: Vincent Hatakeyama <6304302+vincent-hatakeyama@users.noreply.github.com> --- backend/src/hatchling/builders/config.py | 11 +++++------ docs/config/build.md | 9 +++++++++ docs/history/hatchling.md | 1 + tests/backend/builders/test_config.py | 13 +++++++------ 4 files changed, 22 insertions(+), 12 deletions(-) diff --git a/backend/src/hatchling/builders/config.py b/backend/src/hatchling/builders/config.py index d052985a3..d44cfa1e9 100644 --- a/backend/src/hatchling/builders/config.py +++ b/backend/src/hatchling/builders/config.py @@ -675,11 +675,7 @@ def sources(self) -> dict[str, str]: sources[normalize_relative_directory(source)] = '' elif isinstance(raw_sources, dict): - for i, (source, path) in enumerate(raw_sources.items(), 1): - if not source: - message = f'Source #{i} in field `{sources_location}` cannot be an empty string' - raise ValueError(message) - + for source, path in raw_sources.items(): if not isinstance(path, str): message = f'Path for source `{source}` in field `{sources_location}` must be a string' raise TypeError(message) @@ -690,7 +686,7 @@ def sources(self) -> dict[str, str]: else: normalized_path += os.sep - sources[normalize_relative_directory(source)] = normalized_path + sources[normalize_relative_directory(source) if source else source] = normalized_path else: message = f'Field `{sources_location}` must be a mapping or array of strings' raise TypeError(message) @@ -806,6 +802,9 @@ def only_include(self) -> dict[str, str]: def get_distribution_path(self, relative_path: str) -> str: # src/foo/bar.py -> foo/bar.py for source, replacement in self.sources.items(): + if not source: + return replacement + relative_path + if relative_path.startswith(source): return relative_path.replace(source, replacement, 1) diff --git a/docs/config/build.md b/docs/config/build.md index ede9c3cf8..7ef2d4665 100644 --- a/docs/config/build.md +++ b/docs/config/build.md @@ -152,6 +152,15 @@ If you want to remove path prefixes entirely, rather than setting each to an emp sources = ["src"] ``` +If you want to add a prefix to paths, you can use an empty string. For example, the following configuration: + +```toml config-example +[tool.hatch.build.sources] +"" = "foo" +``` + +would distribute the file `bar/file.ext` as `foo/bar/file.ext`. + The [packages](#packages) option itself relies on sources. Defining `#!toml packages = ["src/foo"]` for the `wheel` target is equivalent to the following: ```toml config-example diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index f567cf45c..e64f57f6d 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), - Fix parsing dependencies for environments when warnings are emitted - Properly handle non-zero version epoch for the `standard` version scheme +- Allow using an empty string for the `sources` option to add a prefix to distribution paths ## [1.18.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.18.0) - 2023-06-12 ## {: #hatchling-v1.18.0 } diff --git a/tests/backend/builders/test_config.py b/tests/backend/builders/test_config.py index 5f6ec31c0..45b750568 100644 --- a/tests/backend/builders/test_config.py +++ b/tests/backend/builders/test_config.py @@ -684,8 +684,10 @@ def test_global_mapping_source_empty_string(self, isolation): config = {'tool': {'hatch': {'build': {'sources': {'': 'renamed'}}}}} builder = MockBuilder(str(isolation), config=config) - with pytest.raises(ValueError, match='Source #1 in field `tool.hatch.build.sources` cannot be an empty string'): - _ = builder.config.sources + assert len(builder.config.sources) == 1 + assert builder.config.sources[''] == pjoin('renamed', '') + assert builder.config.get_distribution_path('bar.py') == pjoin('renamed', 'bar.py') + assert builder.config.get_distribution_path(pjoin('foo', 'bar.py')) == pjoin('renamed', 'foo', 'bar.py') def test_global_mapping_path_empty_string(self, isolation): config = {'tool': {'hatch': {'build': {'sources': {'src/foo': ''}}}}} @@ -757,10 +759,9 @@ def test_target_mapping_source_empty_string(self, isolation): builder = MockBuilder(str(isolation), config=config) builder.PLUGIN_NAME = 'foo' - with pytest.raises( - ValueError, match='Source #1 in field `tool.hatch.build.targets.foo.sources` cannot be an empty string' - ): - _ = builder.config.sources + assert len(builder.config.sources) == 1 + assert builder.config.sources[''] == pjoin('renamed', '') + assert builder.config.get_distribution_path(pjoin('bar.py')) == pjoin('renamed', 'bar.py') def test_target_mapping_path_empty_string(self, isolation): config = {'tool': {'hatch': {'build': {'targets': {'foo': {'sources': {'src/foo': ''}}}}}}}