Repository: DavidVujic/python-polylith

Issue:
Breaking change in 1.3.0
**Describe the bug**
The some of my build stopped working with the newest version (1.3.0) of hatch-polylith-bricks

My project use a docker build in CI that suddenly started failing. I was able to trace it back to the newest update of the hatch plugin. Pinning it to 1.2.10 resolved the issue.

The error message also mention editable build which seems to be part of the newest release.

Let me know if I can provide more information to help.

**To Reproduce**
I don't have yet any reproduce step to send as I'd need to take appart my repo to be able to build a MWE. Let me know if I can send anything helpful to debug that. In the mean time here is the project pyproject and docker file
```toml
[build-system]
requires = ["hatchling", "hatch-polylith-bricks == 1.2.10"]
build-backend = "hatchling.build"

[project]
name = "analytics_aggregator"
version = "0.1.0"
requires-python = ">=3.13"
dependencies = [
    "boto3>=1.34.157",
    "polars-lts-cpu>=1.9.0",
    "psycopg[binary]>=3.2.1",
    "pycountry>=24.6.1",
    "pydantic-settings>=2.6.0",
    "pymssql>=2.3.1",
    "sqlalchemy[asyncio]>=2.0.32",
]

[project.scripts]
analytics_aggregator = "bongo.analytics_aggregator:main"

[tool.hatch.build.hooks.polylith-bricks]

[tool.polylith.bricks]
"../../bases/bongo/analytics_aggregator" = "bongo/analytics_aggregator"
"../../components/bongo/services" = "bongo/services"
"../../components/bongo/base_schema" = "bongo/base_schema"
"../../components/bongo/database" = "bongo/database"
"../../components/bongo/analytics_database" = "bongo/analytics_database"
```

```dockerfile
FROM ghcr.io/astral-sh/uv:0.5.5 AS uv
FROM python:3.13.0-slim

# Enable bytecode compilation
ENV UV_COMPILE_BYTECODE=1
# Copy from the cache instead of linking since it's a mounted volume
ENV UV_LINK_MODE=copy
WORKDIR /app

# Install the project's dependencies using the lockfile and settings
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=projects/analytics_aggregator/pyproject.toml,target=projects/analytics_aggregator/pyproject.toml \
    --mount=from=uv,source=/uv,target=/bin/uv \
    uv sync --package analytics_aggregator --frozen --no-install-workspace --no-dev

# Install the project itself aftward to optimize cache
RUN --mount=type=cache,target=/root/.cache/uv \
    --mount=type=bind,source=uv.lock,target=uv.lock \
    --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
    --mount=type=bind,source=projects/analytics_aggregator/pyproject.toml,target=projects/analytics_aggregator/pyproject.toml \
    --mount=type=bind,source=components/,target=components/ \
    --mount=type=bind,source=bases/,target=bases/ \
    --mount=from=uv,source=/uv,target=/bin/uv \
    uv sync --package analytics_aggregator --frozen --no-dev --reinstall

ENV PATH="/app/.venv/bin:$PATH"
CMD ["analytics_aggregator"]
```

**Error Log**
```
 > [stage-1 4/4] RUN --mount=type=cache,target=/root/.cache/uv     --mount=type=bind,source=uv.lock,target=uv.lock     --mount=type=bind,source=pyproject.toml,target=pyproject.toml     --mount=type=bind,source=projects/analytics_aggregator/pyproject.toml,target=projects/analytics_aggregator/pyproject.toml     --mount=type=bind,source=components/,target=components/     --mount=type=bind,source=bases/,target=bases/     --mount=from=uv,source=/uv,target=/bin/uv     uv sync --package analytics_aggregator --frozen --no-dev --reinstall:                                                                          
0.930   × Failed to build `analytics-aggregator @
0.930   │ file:///app/projects/analytics_aggregator`
0.930   ╰─▶ Build backend failed to build editable through `build_editable` (exit
0.930       status: 1)
0.930 
0.930       [stderr]
0.930       Traceback (most recent call last):
0.930         File "<string>", line 11, in <module>
0.930           wheel_filename =
0.930       backend.build_editable("/root/.cache/uv/builds-v0/.tmpztOQgM", {}, None)
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/build.py",
0.930       line 83, in build_editable
0.930           return os.path.basename(next(builder.build(directory=wheel_directory,
0.930       versions=['editable'])))
0.930       
0.930       ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/plugin/interface.py",
0.930       line 155, in build
0.930           artifact = version_api[version](directory, **build_data)
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/wheel.py",
0.930       line 496, in build_editable
0.930           return self.build_editable_detection(directory, **build_data)
0.930                  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/wheel.py",
0.930       line 507, in build_editable_detection
0.930           for included_file in self.recurse_selected_project_files():
0.930                                ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/plugin/interface.py",
0.930       line 180, in recurse_selected_project_files
0.930           if self.config.only_include:
0.930              ^^^^^^^^^^^^^^^^^^^^^^^^
0.930         File "/usr/local/lib/python3.13/functools.py", line 1037, in __get__
0.930           val = self.func(instance)
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/config.py",
0.930       line 713, in only_include
0.930           only_include = only_include_config.get('only-include',
0.930       self.default_only_include()) or self.packages
0.930       
0.930       ~~~~~~~~~~~~~~~~~~~~~~~~~^^
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/wheel.py",
0.930       line 262, in default_only_include
0.930           return self.default_file_selection_options.only_include
0.930                  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
0.930         File "/usr/local/lib/python3.13/functools.py", line 1037, in __get__
0.930           val = self.func(instance)
0.930         File
0.930       "/root/.cache/uv/builds-v0/.tmppaQgwW/lib/python3.13/site-packages/hatchling/builders/wheel.py",
0.930       line 250, in default_file_selection_options
0.930           raise ValueError(message)
0.930       ValueError: Unable to determine which files to ship
0.930       inside the wheel using the following heuristics:
0.930       https://hatch.pypa.io/latest/plugins/builder/wheel/#default-file-selection
0.930 
0.930       The most likely cause of this is that there is no directory that matches
0.930       the name of your project (analytics_aggregator).
0.930 
0.930       At least one file selection option must be defined
0.930       in the `tool.hatch.build.targets.wheel` table, see:
0.930       https://hatch.pypa.io/latest/config/build/
0.930 
0.930       As an example, if you intend to ship a directory named `foo` that
0.930       resides within a `src` directory located at the root of your project,
0.930       you can define the following:
0.930 
0.930       [tool.hatch.build.targets.wheel]
0.930       packages = ["src/foo"]
0.930 
```

**Desktop (please complete the following information):**
 - OS: Linux
 - Python version: 3.13
 - UV version 0.5.5

Hints from issue comments:
@DavidVujic: Oh, I’m sorry! I think I know the reason and why I didn’t notice the problem in my setup. I will work on this issue right away and release a new version shortly.

Great that you have found a workaround by pinning the version!

@DavidVujic: I _think_ that I have solved the issue in the just released v1.3.1 of the Hatch build hook. Please let me know if it solved it or if you still have issues 🙏 @enadeau: I've just tested it and the issue is gone. Thanks for the quick response! 

Solution patch:
diff --git a/components/polylith/hatch/hooks/bricks.py b/components/polylith/hatch/hooks/bricks.py
index 2d3149ba..ae8233d1 100644
--- a/components/polylith/hatch/hooks/bricks.py
+++ b/components/polylith/hatch/hooks/bricks.py
@@ -1,22 +1,49 @@
 import shutil
 from pathlib import Path
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 from hatchling.builders.hooks.plugin.interface import BuildHookInterface
 from polylith import parsing, repo, toml
 from polylith.hatch import core
 
 
+def get_build_section(data: dict) -> dict:
+    return data.get("tool", {}).get("hatch", {}).get("build", {})
+
+
+def is_in_path(key: str, paths: List[str]) -> bool:
+    return any(key.startswith(p) for p in paths)
+
+
+def filter_dev_mode_bricks(data: dict, bricks: dict) -> dict:
+    build_section = get_build_section(data)
+    dev_mode_dirs = build_section.get("dev-mode-dirs")
+
+    if not dev_mode_dirs:
+        return bricks
+
+    return {k: v for k, v in bricks.items() if not is_in_path(k, dev_mode_dirs)}
+
+
+def filtered_bricks(data: dict, version: str) -> dict:
+    bricks = toml.get_project_packages_from_polylith_section(data)
+
+    if version == "editable":
+        return filter_dev_mode_bricks(data, bricks)
+
+    return bricks
+
+
 class PolylithBricksHook(BuildHookInterface):
     PLUGIN_NAME = "polylith-bricks"
 
     def initialize(self, version: str, build_data: Dict[str, Any]) -> None:
-        include_key = "force_include_editable" if version == "editable" else "force_include"
+        include_key = "force_include"
         root = self.root
         pyproject = Path(f"{root}/{repo.default_toml}")
 
         data = toml.read_toml_document(pyproject)
-        bricks = toml.get_project_packages_from_polylith_section(data)
+        bricks = filtered_bricks(data, version)
         found_bricks = {k: v for k, v in bricks.items() if Path(f"{root}/{k}").exists()}
 
         if not bricks or not found_bricks:
diff --git a/projects/hatch_polylith_bricks/pyproject.toml b/projects/hatch_polylith_bricks/pyproject.toml
index 60517265..38a1a638 100644
--- a/projects/hatch_polylith_bricks/pyproject.toml
+++ b/projects/hatch_polylith_bricks/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "hatch-polylith-bricks"
-version = "1.3.0"
+version = "1.3.1"
 description = "Hatch build hook plugin for Polylith"
 authors = ['David Vujic']
 homepage = "https://davidvujic.github.io/python-polylith-docs/"


Test patch:
diff --git a/test/components/polylith/hatch/test_hook.py b/test/components/polylith/hatch/test_hook.py
new file mode 100644
index 00000000..0c47f048
--- /dev/null
+++ b/test/components/polylith/hatch/test_hook.py
@@ -0,0 +1,41 @@
+import tomlkit
+from polylith.hatch.hooks.bricks import filtered_bricks
+
+project_toml = """\
+[tool.polylith.bricks]
+"../../bases/unittest/one" = "unittest/one"
+"../../components/unittest/two" = "unittest/two"
+"""
+
+project_toml_dev_mode = """\
+[tool.hatch.build]
+dev-mode-dirs = ["../../components", "../../bases"]
+
+[tool.polylith.bricks]
+"../../bases/unittest/one" = "unittest/one"
+"../../components/unittest/two" = "unittest/two"
+"""
+
+
+def test_filtered_bricks():
+    data = tomlkit.loads(project_toml)
+
+    expected = data["tool"]["polylith"]["bricks"]
+
+    first = filtered_bricks(data, version="standard")
+    second = filtered_bricks(data, version="editable")
+
+    assert first == expected
+    assert second == expected
+
+
+def test_filtered_bricks_in_project_with_dev_mode_dirs():
+    data = tomlkit.loads(project_toml_dev_mode)
+
+    expected = data["tool"]["polylith"]["bricks"]
+
+    first = filtered_bricks(data, version="standard")
+    second = filtered_bricks(data, version="editable")
+
+    assert first == expected
+    assert second == {}