diff --git a/pyproject.toml b/pyproject.toml index 257957bb2..8088b8b5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,7 +54,7 @@ classifiers = [ "pre-commit~=4.0", "ruff~=0.15", "pandas-stubs~=2.3", - "pyright~=1.1", + "pyright[nodejs]~=1.1", "build~=1.4", "vulture==2.14", "pytest~=9.0", diff --git a/tests/test_check_copyright_headers.py b/tests/test_check_copyright_headers.py index dd3fe1d9e..dd611de89 100644 --- a/tests/test_check_copyright_headers.py +++ b/tests/test_check_copyright_headers.py @@ -96,63 +96,49 @@ def run_git(cmd: list[str]) -> str: def collect_years_same_file(path: Path) -> list[int]: - """ - Using --follow but only following file movements (not copying) with more than 92% similarity. + """Follow high-similarity renames explicitly; copied files start their own history. - 92% appears when one moves an empty __init__.py to another folder and updates years in the license header. + Restart history tracking at each rename so Git's merge traversal cannot + switch to an unrelated path. A 92% threshold allows moving an empty __init__.py and + updating its copyright year without following larger rewrites. """ - git_output = run_git( - [ - "log", - "--follow", - "--name-status", - "--find-renames=92%", - "--format=%x1e%ad", - "--date=format:%Y", - "--", - path.as_posix(), - ], - ) - years: set[int] = set() - current_path = path.as_posix() - - for rec in git_output.split(_REC_SEP): - rec = rec.strip() - if not rec: - continue - - lines = [ln.strip() for ln in rec.splitlines() if ln.strip()] - - year = int(lines[0]) - parts = lines[1].split("\t") - commit_status = parts[0] - - # A copied file starts its own history; record its creation year without following the source. - if len(commit_status) == 4 and commit_status[0] == "C" and len(parts) == 3 and parts[2] == current_path: - years.add(year) - break - - # Follow only exact rename hops for the currently tracked path. - if len(commit_status) == 4 and commit_status[0] == "R" and len(parts) == 3: - percentage = int(commit_status[1:]) - old_path, new_path = parts[1], parts[2] - if percentage >= 92 and new_path == current_path: - current_path = old_path - years.add(year) + current_path: str | None = path.as_posix() + revision = "HEAD" + while current_path is not None: + git_output = run_git( + [ + "log", + "--follow", + "--name-status", + "--find-renames=92%", + "--format=%x1e%H %ad", + "--date=format:%Y", + revision, + "--", + current_path, + ] + ) + next_path = None + for rec in git_output.split(_REC_SEP): + lines = [line.strip() for line in rec.splitlines() if line.strip()] + if not lines: continue - - # Normal touch of current path. - elif len(parts) >= 2 and parts[1] == current_path: - years.add(year) - continue - - break - - git_status = run_git(["status", "--porcelain", "--", path.as_posix()]) - if git_status.strip(): + commit, year = lines[0].split() + changes = [line.split("\t") for line in lines[1:]] + change = next((parts for parts in changes if len(parts) >= 2 and parts[-1] == current_path), None) + if change is None: + continue + years.add(int(year)) + if change[0].startswith("R"): + next_path = change[1] + revision = f"{commit}^" + break + if change[0].startswith("C"): + break + current_path = next_path + if run_git(["status", "--porcelain", "--", path.as_posix()]).strip(): years.add(CURRENT_YEAR) - return sorted(years) diff --git a/uv.lock b/uv.lock index 9780031be..4d6b744c2 100644 --- a/uv.lock +++ b/uv.lock @@ -325,7 +325,7 @@ dev = [ { name = "import-linter" }, { name = "pandas-stubs" }, { name = "pre-commit" }, - { name = "pyright" }, + { name = "pyright", extra = ["nodejs"] }, { name = "pytest" }, { name = "pytest-cov" }, { name = "pytest-deadfixtures" }, @@ -382,7 +382,7 @@ requires-dist = [ { name = "pandas-stubs", marker = "extra == 'dev'", specifier = "~=2.3" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = "~=4.0" }, { name = "pydantic", specifier = "~=2.12" }, - { name = "pyright", marker = "extra == 'dev'", specifier = "~=1.1" }, + { name = "pyright", extras = ["nodejs"], marker = "extra == 'dev'", specifier = "~=1.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = "~=9.0" }, { name = "pytest-cov", marker = "extra == 'dev'", specifier = "~=7.0" }, { name = "pytest-deadfixtures", marker = "extra == 'dev'", specifier = "~=3.1" }, @@ -1372,6 +1372,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, ] +[[package]] +name = "nodejs-wheel-binaries" +version = "24.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/76/7e97195e14346598565a0de4ca8bdbd5e634b3fb5b1ba590b7b1b89f8a63/nodejs_wheel_binaries-24.19.0.tar.gz", hash = "sha256:db217eef8cab8551667863379b08db4d9067403f6cbbe87481eb40edceb8aa9b", size = 8058, upload-time = "2026-08-19T21:47:19.671Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/52/0774b52c7be8151ad9d5aff44edc100c3f13d6d8eb3765f63ffa40e69fe8/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:e12cbfd69089504e42fb14194ce734a9dcf3eb38c820ca63dd511d36fb964e9c", size = 56047203, upload-time = "2026-08-19T21:46:43.448Z" }, + { url = "https://files.pythonhosted.org/packages/67/3a/4fdbbfecf2c23d52c0e3f68de7f7c1b3c97a26d328c69c5f6c49c48e340e/nodejs_wheel_binaries-24.19.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:1c890adf4b7e6556ccc1ca66c866bb81884b6c9a581dee4e530dc7f78fb9d514", size = 56219459, upload-time = "2026-08-19T21:46:48.45Z" }, + { url = "https://files.pythonhosted.org/packages/5f/a8/0147149415195c59b8a72a594916bfb80d6be4d586f9fbfda313889e0efc/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:4e029dadfae1295876063c96b236f673487e8c27379fe146c1e2250283520227", size = 60588256, upload-time = "2026-08-19T21:46:53.298Z" }, + { url = "https://files.pythonhosted.org/packages/f4/89/6631d0982353da1bb7bc00bb1988f702822c62b42634f57999ee53b5c337/nodejs_wheel_binaries-24.19.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:4196a947bcc883f2003ab101762d729f3e99b5e86b75bd09151563403e2eceb8", size = 61123607, upload-time = "2026-08-19T21:46:58.117Z" }, + { url = "https://files.pythonhosted.org/packages/32/a2/fa30f0841e4602995782e124359f9b910c7b481d98decf61ef0b2fc3ebfb/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:352e048ab4dd35e7de5f338d1cc4fcbf77a0e93da30bf7336a8217ee246b31d7", size = 62632842, upload-time = "2026-08-19T21:47:03.42Z" }, + { url = "https://files.pythonhosted.org/packages/18/01/22d97ca72213f66cc386ee638db30c2e62757fdf761c6029074bced83d1c/nodejs_wheel_binaries-24.19.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:28d078b2ced9e2069516e652dc4b1380e7a1a7f2d3934eccd1611586d283ba4c", size = 63250653, upload-time = "2026-08-19T21:47:07.938Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/e3be8fa327a795bcaf7a19cd84299e338a7bce32ff0665fdce9cfa22573c/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_amd64.whl", hash = "sha256:67e3abeb9c3830cae8c8487ae8a2af7cc27dfa75af06145cee5ca7d1857c81bd", size = 42448503, upload-time = "2026-08-19T21:47:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/1d/37/34cf28ba1691a060174948a9927fe61091982d6048b2e403071a9acce443/nodejs_wheel_binaries-24.19.0-py2.py3-none-win_arm64.whl", hash = "sha256:d9074c665ea68b04e183d82482c86dc907d3a9bd15eb6cf85542cb785266bb36", size = 40090155, upload-time = "2026-08-19T21:47:16.032Z" }, +] + [[package]] name = "numpy" version = "2.2.6" @@ -2023,6 +2039,11 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, ] +[package.optional-dependencies] +nodejs = [ + { name = "nodejs-wheel-binaries" }, +] + [[package]] name = "pytest" version = "9.0.3"