Skip to content

Commit 44e7c4e

Browse files
l0lawrenceCopilot
andcommitted
reuse discover_targetd_packagfes
Co-authored-by: Copilot <copilot@github.com>
1 parent 303c66d commit 44e7c4e

2 files changed

Lines changed: 111 additions & 67 deletions

File tree

eng/tools/azure-sdk-tools/azpysdk/changelog.py

Lines changed: 50 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@
66
from typing import Optional, List
77

88
from .Check import Check, REPO_ROOT
9+
from ci_tools.functions import discover_targeted_packages
910
from ci_tools.logging import logger
11+
from ci_tools.parsing import ParsedSetup
1012

1113
# The expected Chronus package name and on-disk install location.
1214
# Chronus is pinned as a dev dependency in .github/chronus/package.json with
@@ -211,13 +213,17 @@ def _ensure_chronus_installed(self) -> None:
211213
)
212214
raise SystemExit(1)
213215

214-
def _detect_package_from_cwd(self) -> Optional[str]:
215-
"""If CWD is inside a package directory (``sdk/<service>/<package>``),
216-
return the Chronus package **name** (the directory basename, e.g.
217-
``azure-core``). Otherwise return ``None``.
216+
def _find_package_root_from_cwd(self) -> Optional[str]:
217+
"""Find the package root directory when CWD is at or below ``sdk/<service>/<package>``.
218218
219-
The chronus config uses the pattern ``sdk/*/*`` for packages, so we
220-
look for CWD being at or below ``<REPO_ROOT>/sdk/<service>/<pkg>``.
219+
Walks up from the current directory to locate the package root,
220+
unlike ``get_targeted_directories(target=".")`` which only works
221+
when CWD is exactly the package root. This lets developers run
222+
changelog commands from subdirectories such as
223+
``sdk/core/azure-core/tests/``.
224+
225+
Returns the absolute path to the package root, or ``None`` if CWD
226+
is not inside an ``sdk/<service>/<package>`` tree.
221227
"""
222228
try:
223229
cwd = os.path.abspath(os.getcwd())
@@ -227,40 +233,38 @@ def _detect_package_from_cwd(self) -> Optional[str]:
227233
# On Windows, relpath raises ValueError when paths are on different drives
228234
return None
229235

230-
# rel should start with "sdk/<service>/<package>" (at least 3 components)
231236
parts = rel.replace("\\", "/").split("/")
232237
if len(parts) >= 3 and parts[0] == "sdk":
233-
# Return the package name (third component, e.g. "azure-core")
234-
return parts[2]
238+
return os.path.join(repo, parts[0], parts[1], parts[2])
235239
return None
236240

237-
def _resolve_package_name(self, package: str) -> str:
238-
"""Resolve a user-supplied package argument to a Chronus package name.
239-
240-
Accepts either:
241-
- A package name directly (e.g. ``azure-core``) — returned as-is.
242-
- A relative or absolute path (e.g. ``sdk/core/azure-core``,
243-
``.\\sdk\\core\\azure-core\\``) — resolved to the package name.
241+
def _resolve_package(self, package_arg: Optional[str]) -> Optional[str]:
242+
"""Resolve a package argument or CWD to a Chronus package name.
244243
245-
Chronus identifies packages by name (the directory basename under
246-
``sdk/<service>/``), not by path.
244+
Uses ``discover_targeted_packages`` — the same discovery function
245+
that powers ``get_targeted_directories`` — to locate packages by
246+
path or bare name. When *package_arg* is ``None``, the method
247+
detects the package from CWD by walking up to the nearest
248+
``sdk/<service>/<package>`` directory.
247249
"""
248-
# If the path is absolute or starts with '.' resolve it relative to repo root
249-
if os.path.isabs(package) or package.startswith("."):
250-
try:
251-
abs_path = os.path.abspath(os.path.join(os.getcwd(), package))
252-
repo = os.path.abspath(REPO_ROOT)
253-
package = os.path.relpath(abs_path, repo)
254-
except ValueError:
255-
pass
256-
# Normalize separators and strip trailing slashes
257-
package = package.replace("\\", "/").strip("/")
258-
# If it looks like a path (sdk/<service>/<package>...), extract the name
259-
parts = package.split("/")
260-
if len(parts) >= 3 and parts[0] == "sdk":
261-
return parts[2]
262-
# Otherwise assume it's already a package name
263-
return package
250+
if package_arg:
251+
found = discover_targeted_packages(package_arg, REPO_ROOT)
252+
if found:
253+
try:
254+
return ParsedSetup.from_path(found[0]).name
255+
except Exception:
256+
return os.path.basename(found[0])
257+
# Not found by discovery — pass through as-is
258+
return package_arg
259+
260+
# No explicit package — detect from CWD
261+
pkg_root = self._find_package_root_from_cwd()
262+
if pkg_root is None:
263+
return None
264+
try:
265+
return ParsedSetup.from_path(pkg_root).name
266+
except Exception:
267+
return os.path.basename(os.path.normpath(pkg_root))
264268

265269
def _run_chronus(self, chronus_args: List[str]) -> int:
266270
"""Run a chronus command from the repository root.
@@ -293,13 +297,10 @@ def _run_add(self, args: argparse.Namespace) -> int:
293297
``azpysdk changelog add --kind breaking -m "Removed foo API"``).
294298
"""
295299
chronus_args = ["add"]
296-
package = args.package
297-
if package:
298-
package = self._resolve_package_name(package)
299-
else:
300-
package = self._detect_package_from_cwd()
301-
if package:
302-
logger.info(f"Detected package from current directory: {package}")
300+
detected_from_cwd = not args.package
301+
package = self._resolve_package(args.package)
302+
if package and detected_from_cwd:
303+
logger.info(f"Detected package from current directory: {package}")
303304
if package:
304305
chronus_args.append(package)
305306

@@ -323,13 +324,10 @@ def _run_create(self, args: argparse.Namespace) -> int:
323324
via ``--package`` so only that package's changelog is generated.
324325
"""
325326
chronus_args = ["changelog"]
326-
package = args.package
327-
if package:
328-
package = self._resolve_package_name(package)
329-
else:
330-
package = self._detect_package_from_cwd()
331-
if package:
332-
logger.info(f"Detected package from current directory: {package}")
327+
detected_from_cwd = not args.package
328+
package = self._resolve_package(args.package)
329+
if package and detected_from_cwd:
330+
logger.info(f"Detected package from current directory: {package}")
333331
if not package:
334332
logger.error(
335333
"No package specified and could not detect one from the current directory.\n"
@@ -356,13 +354,10 @@ def _run_status(self, args: argparse.Namespace) -> int:
356354
via ``--only`` so only that package's status is shown.
357355
"""
358356
chronus_args = ["status"]
359-
package = args.package
360-
if package:
361-
package = self._resolve_package_name(package)
362-
else:
363-
package = self._detect_package_from_cwd()
364-
if package:
365-
logger.info(f"Detected package from current directory: {package}")
357+
detected_from_cwd = not args.package
358+
package = self._resolve_package(args.package)
359+
if package and detected_from_cwd:
360+
logger.info(f"Detected package from current directory: {package}")
366361
if package:
367362
chronus_args.extend(["--only", package])
368363
return self._run_chronus(chronus_args)

eng/tools/azure-sdk-tools/tests/test_changelog_commands.py

Lines changed: 61 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import argparse
44
import os
55
import sys
6+
from types import SimpleNamespace
67
from unittest.mock import patch, MagicMock, call
78

89
import pytest
@@ -303,7 +304,8 @@ def test_create_calls_chronus_changelog(self, mock_call, _mock_installed):
303304
def test_status_calls_chronus_status(self, mock_call, _mock_installed):
304305
parser = _build_parser()
305306
args = parser.parse_args(["changelog", "status"])
306-
result = args.func(args)
307+
with patch("os.getcwd", return_value=REPO_ROOT):
308+
result = args.func(args)
307309
assert result == 0
308310
cmd = mock_call.call_args[0][0]
309311
assert cmd == [_CHRONUS_BIN, "status"]
@@ -328,42 +330,89 @@ def test_nonzero_exit_code_propagated(self, mock_call, _mock_installed):
328330

329331

330332
# ---------------------------------------------------------------------------
331-
# CWD detection unit tests
333+
# Package resolution unit tests
332334
# ---------------------------------------------------------------------------
333335

334336

335-
class TestDetectPackageFromCwd:
336-
"""Test the _detect_package_from_cwd helper directly."""
337+
class TestFindPackageRootFromCwd:
338+
"""Test the _find_package_root_from_cwd helper directly."""
337339

338340
def test_at_package_root(self):
339341
c = changelog()
340-
with patch("os.getcwd", return_value=os.path.join(REPO_ROOT, "sdk", "core", "azure-core")):
341-
assert c._detect_package_from_cwd() == "azure-core"
342+
pkg_root = os.path.join(REPO_ROOT, "sdk", "core", "azure-core")
343+
with patch("os.getcwd", return_value=pkg_root):
344+
assert c._find_package_root_from_cwd() == pkg_root
342345

343346
def test_inside_package_subdir(self):
344347
c = changelog()
345-
with patch("os.getcwd", return_value=os.path.join(REPO_ROOT, "sdk", "core", "azure-core", "azure", "core")):
346-
assert c._detect_package_from_cwd() == "azure-core"
348+
pkg_root = os.path.join(REPO_ROOT, "sdk", "core", "azure-core")
349+
with patch("os.getcwd", return_value=os.path.join(pkg_root, "azure", "core")):
350+
assert c._find_package_root_from_cwd() == pkg_root
347351

348352
def test_at_repo_root(self):
349353
c = changelog()
350354
with patch("os.getcwd", return_value=REPO_ROOT):
351-
assert c._detect_package_from_cwd() is None
355+
assert c._find_package_root_from_cwd() is None
352356

353357
def test_at_sdk_dir(self):
354358
c = changelog()
355359
with patch("os.getcwd", return_value=os.path.join(REPO_ROOT, "sdk")):
356-
assert c._detect_package_from_cwd() is None
360+
assert c._find_package_root_from_cwd() is None
357361

358362
def test_at_service_dir(self):
359363
c = changelog()
360364
with patch("os.getcwd", return_value=os.path.join(REPO_ROOT, "sdk", "storage")):
361-
assert c._detect_package_from_cwd() is None
365+
assert c._find_package_root_from_cwd() is None
362366

363367
def test_outside_repo(self):
364368
c = changelog()
365369
with patch("os.getcwd", return_value="/tmp"):
366-
assert c._detect_package_from_cwd() is None
370+
assert c._find_package_root_from_cwd() is None
371+
372+
373+
class TestResolvePackage:
374+
"""Test the _resolve_package helper that uses discover_targeted_packages."""
375+
376+
@patch("azpysdk.changelog.ParsedSetup")
377+
@patch("azpysdk.changelog.discover_targeted_packages")
378+
def test_explicit_path_uses_discover(self, mock_discover, mock_parsed_setup):
379+
mock_discover.return_value = [os.path.join(REPO_ROOT, "sdk", "core", "azure-core")]
380+
mock_parsed_setup.from_path.return_value = SimpleNamespace(name="azure-core")
381+
c = changelog()
382+
result = c._resolve_package("sdk/core/azure-core")
383+
assert result == "azure-core"
384+
mock_discover.assert_called_once_with("sdk/core/azure-core", REPO_ROOT)
385+
386+
@patch("azpysdk.changelog.ParsedSetup")
387+
@patch("azpysdk.changelog.discover_targeted_packages")
388+
def test_bare_name_uses_discover(self, mock_discover, mock_parsed_setup):
389+
mock_discover.return_value = [os.path.join(REPO_ROOT, "sdk", "core", "azure-core")]
390+
mock_parsed_setup.from_path.return_value = SimpleNamespace(name="azure-core")
391+
c = changelog()
392+
result = c._resolve_package("azure-core")
393+
assert result == "azure-core"
394+
mock_discover.assert_called_once_with("azure-core", REPO_ROOT)
395+
396+
@patch("azpysdk.changelog.discover_targeted_packages")
397+
def test_bare_name_passthrough_when_not_discovered(self, mock_discover):
398+
mock_discover.return_value = []
399+
c = changelog()
400+
result = c._resolve_package("some-nonexistent-package-name")
401+
assert result == "some-nonexistent-package-name"
402+
403+
@patch("azpysdk.changelog.ParsedSetup")
404+
def test_cwd_detection_uses_parsed_setup(self, mock_parsed_setup):
405+
mock_parsed_setup.from_path.return_value = SimpleNamespace(name="azure-storage-blob")
406+
c = changelog()
407+
pkg_dir = os.path.join(REPO_ROOT, "sdk", "storage", "azure-storage-blob")
408+
with patch("os.getcwd", return_value=pkg_dir):
409+
result = c._resolve_package(None)
410+
assert result == "azure-storage-blob"
411+
412+
def test_cwd_at_repo_root_returns_none(self):
413+
c = changelog()
414+
with patch("os.getcwd", return_value=REPO_ROOT):
415+
assert c._resolve_package(None) is None
367416

368417

369418
# ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)