Skip to content

Commit fe3b8a6

Browse files
Fix usethis tool failing in directories with leading dots (#1903)
* Initial plan * Fix uv/poetry init failing in directories with leading dots in their name Pass --name with a sanitized project name to uv init, and fix get_project_name_from_dir() to strip leading/trailing non-alphanumeric characters (dots, dashes, underscores) per PEP packaging spec. Also update poetry init to use get_project_name_from_dir() instead of the raw directory name for consistency. Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/e415c235-da21-4847-b92c-704513c59e6e Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Fix Windows test failure: use PurePosixPath instead of real directories The test_only_dots test created a directory named "..." which on Windows is interpreted as parent directory traversal (../..), causing FileExistsError. Since get_project_name_from_dir() only reads Path.name and never accesses the filesystem, all tests now use PurePosixPath synthetic paths instead of creating real directories. Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/b47fb40c-7e7c-44f9-89e3-258b10a5fe34 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> * Fix basedpyright: replace PurePosixPath with Path/str in tests PurePosixPath is not assignable to Path | str | None. Use tmp_path / "name" (Path without mkdir) for most tests, and string paths for Windows-problematic names ("..." and "project.") since usethis_config .set() accepts str and converts to Path internally. Agent-Logs-Url: https://github.com/usethis-python/usethis-python/sessions/06111449-4dd0-43b5-87cf-f13293af9a57 Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: nathanjmcdougall <18602289+nathanjmcdougall@users.noreply.github.com>
1 parent bda7f18 commit fe3b8a6

6 files changed

Lines changed: 95 additions & 6 deletions

File tree

src/usethis/_backend/poetry/init.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from usethis._backend.poetry.call import call_poetry_subprocess
88
from usethis._backend.poetry.errors import PoetryInitError, PoetrySubprocessFailedError
9-
from usethis._config import usethis_config
9+
from usethis._file.dir import get_project_name_from_dir
1010
from usethis._file.pyproject_toml.errors import PyprojectTOMLInitError
1111

1212

@@ -32,7 +32,7 @@ def ensure_pyproject_toml_via_poetry(*, author: bool = True) -> None:
3232
args = [
3333
"init",
3434
"--name",
35-
usethis_config.cpd().name,
35+
get_project_name_from_dir(),
3636
"--python",
3737
_get_poetry_python_constraint(),
3838
]
@@ -54,7 +54,7 @@ def opinionated_poetry_init() -> None:
5454
[
5555
"init",
5656
"--name",
57-
usethis_config.cpd().name,
57+
get_project_name_from_dir(),
5858
"--python",
5959
_get_poetry_python_constraint(),
6060
],

src/usethis/_backend/uv/init.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
)
88
from usethis._backend.uv.errors import UVInitError, UVSubprocessFailedError
99
from usethis._config import usethis_config
10+
from usethis._file.dir import get_project_name_from_dir
1011
from usethis._file.pyproject_toml.errors import PyprojectTOMLInitError
1112

1213

@@ -20,6 +21,8 @@ def opinionated_uv_init() -> None:
2021
[
2122
"init",
2223
"--lib",
24+
"--name",
25+
get_project_name_from_dir(),
2326
"--build-backend",
2427
usethis_config.build_backend.value,
2528
usethis_config.cpd().as_posix(),
@@ -45,6 +48,8 @@ def ensure_pyproject_toml_via_uv(*, author: bool = True) -> None:
4548
"--bare",
4649
"--vcs=none",
4750
f"--author-from={author_from}",
51+
"--name",
52+
get_project_name_from_dir(),
4853
"--build-backend",
4954
usethis_config.build_backend.value,
5055
usethis_config.cpd().as_posix(),

src/usethis/_file/dir.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ def get_project_name_from_dir() -> str:
77
"""Derive a valid project name from the current directory name."""
88
# Use the name of the parent directory
99
# Names must start and end with a letter or digit and may only contain -, _, ., and
10-
# alphanumeric characters. Any other characters will be dropped. If there are no
11-
# valid characters, the name will be "hello_world".
10+
# alphanumeric characters. Any other characters will be dropped. Leading and trailing
11+
# non-alphanumeric characters are stripped. If there are no valid characters, the
12+
# name will be "hello_world".
1213
# https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#name
1314
# https://packaging.python.org/en/latest/specifications/name-normalization/#name-format
1415
dir_name = usethis_config.cpd().name
1516
name = "".join(c for c in dir_name if c.isalnum() or c in {"-", "_", "."})
17+
name = name.strip("-_.")
1618
if not name:
1719
name = "hello_world"
1820

tests/usethis/_backend/uv/test_init.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from pathlib import Path
22

33
from _test import change_cwd
4-
from usethis._backend.uv.init import opinionated_uv_init
4+
from usethis._backend.uv.init import ensure_pyproject_toml_via_uv, opinionated_uv_init
55
from usethis._config import usethis_config
66
from usethis._config_file import files_manager
77
from usethis._file.pyproject_toml.io_ import PyprojectTOMLManager
@@ -30,3 +30,27 @@ def test_build_backend_is_uv(self, tmp_path: Path):
3030

3131
# Assert
3232
assert manager[["build-system", "build-backend"]] == "uv_build"
33+
34+
def test_leading_dot_dir_name(self, tmp_path: Path):
35+
path = tmp_path / ".github-private"
36+
path.mkdir()
37+
with change_cwd(path), files_manager():
38+
manager = PyprojectTOMLManager()
39+
# Act
40+
opinionated_uv_init()
41+
42+
# Assert
43+
assert manager[["project", "name"]] == "github-private"
44+
45+
46+
class TestEnsurePyprojectTomlViaUV:
47+
def test_leading_dot_dir_name(self, tmp_path: Path):
48+
path = tmp_path / ".github-private"
49+
path.mkdir()
50+
with change_cwd(path), files_manager():
51+
manager = PyprojectTOMLManager()
52+
# Act
53+
ensure_pyproject_toml_via_uv()
54+
55+
# Assert
56+
assert manager[["project", "name"]] == "github-private"

tests/usethis/_file/pyproject_toml/test_valid.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,26 @@ def test_drop_nonalphanum_chars_from_dir_name(self, tmp_path: Path):
226226
[project]
227227
name = "h-el.l_o"
228228
version = "0.2.0"
229+
"""
230+
)
231+
232+
def test_leading_dot_dir_name(self, tmp_path: Path):
233+
# Arrange
234+
path = tmp_path / ".github-private"
235+
path.mkdir()
236+
(path / "pyproject.toml").write_text("")
237+
238+
# Act
239+
with change_cwd(path), files_manager():
240+
ensure_pyproject_validity()
241+
242+
# Assert
243+
assert (
244+
(path / "pyproject.toml").read_text()
245+
== """\
246+
[project]
247+
name = "github-private"
248+
version = "0.1.0"
229249
"""
230250
)
231251

tests/usethis/_file/test_dir.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
from pathlib import Path
2+
3+
from usethis._config import usethis_config
4+
from usethis._file.dir import get_project_name_from_dir
5+
6+
7+
class TestGetProjectNameFromDir:
8+
def test_simple_name(self, tmp_path: Path):
9+
with usethis_config.set(project_dir=tmp_path / "my_project"):
10+
assert get_project_name_from_dir() == "my_project"
11+
12+
def test_leading_dot(self, tmp_path: Path):
13+
with usethis_config.set(project_dir=tmp_path / ".github-private"):
14+
assert get_project_name_from_dir() == "github-private"
15+
16+
def test_leading_dots(self, tmp_path: Path):
17+
with usethis_config.set(project_dir=tmp_path / "..hidden"):
18+
assert get_project_name_from_dir() == "hidden"
19+
20+
def test_trailing_dot(self):
21+
with usethis_config.set(project_dir="/fake/project."):
22+
assert get_project_name_from_dir() == "project"
23+
24+
def test_leading_and_trailing_non_alphanumeric(self, tmp_path: Path):
25+
with usethis_config.set(project_dir=tmp_path / "-_project_-"):
26+
assert get_project_name_from_dir() == "project"
27+
28+
def test_only_dots(self):
29+
with usethis_config.set(project_dir="/fake/..."):
30+
assert get_project_name_from_dir() == "hello_world"
31+
32+
def test_no_valid_chars(self, tmp_path: Path):
33+
with usethis_config.set(project_dir=tmp_path / "+"):
34+
assert get_project_name_from_dir() == "hello_world"
35+
36+
def test_drops_invalid_chars(self, tmp_path: Path):
37+
with usethis_config.set(project_dir=tmp_path / "h-e+l.l_o"):
38+
assert get_project_name_from_dir() == "h-el.l_o"

0 commit comments

Comments
 (0)