ansys-pre-commit-hooks 0.7.2__tar.gz → 0.8.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/LICENSE +1 -1
  2. {ansys_pre_commit_hooks-0.7.2/src/ansys_pre_commit_hooks.egg-info → ansys_pre_commit_hooks-0.8.0}/PKG-INFO +8 -8
  3. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/pyproject.toml +7 -7
  4. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/setup.py +7 -7
  5. ansys_pre_commit_hooks-0.8.0/src/ansys/pre_commit_hooks/VERSION +1 -0
  6. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/__init__.py +1 -1
  7. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/add_license_headers.py +108 -25
  8. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/assets/LICENSES/Apache-2.0.txt +1 -1
  9. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/assets/LICENSES/MIT.txt +1 -1
  10. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/tech_review.py +3 -3
  11. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/AUTHORS +1 -1
  12. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/LICENSE +1 -1
  13. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0/src/ansys_pre_commit_hooks.egg-info}/PKG-INFO +8 -8
  14. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys_pre_commit_hooks.egg-info/requires.txt +3 -3
  15. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/tests/test_add_license_headers.py +143 -10
  16. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/tests/test_metadata.py +1 -1
  17. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/tests/test_tech_review.py +4 -4
  18. ansys_pre_commit_hooks-0.7.2/src/ansys/pre_commit_hooks/VERSION +0 -1
  19. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/MANIFEST.in +0 -0
  20. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/README.rst +0 -0
  21. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/setup.cfg +0 -0
  22. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/assets/.reuse/templates/ansys.jinja2 +0 -0
  23. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/assets/licenses.json +0 -0
  24. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/CODE_OF_CONDUCT.md +0 -0
  25. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/CONTRIBUTING.md +0 -0
  26. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/CONTRIBUTORS.md +0 -0
  27. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/README.md +0 -0
  28. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/README.rst +0 -0
  29. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys/pre_commit_hooks/templates/dependabot.yml +0 -0
  30. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys_pre_commit_hooks.egg-info/SOURCES.txt +0 -0
  31. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys_pre_commit_hooks.egg-info/dependency_links.txt +0 -0
  32. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys_pre_commit_hooks.egg-info/entry_points.txt +0 -0
  33. {ansys_pre_commit_hooks-0.7.2 → ansys_pre_commit_hooks-0.8.0}/src/ansys_pre_commit_hooks.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
3
+ Copyright (c) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansys-pre-commit-hooks
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Home-page: https://github.com/ansys/pre-commit-hooks
5
- Author: ANSYS, Inc.
6
- Author-email: "ANSYS, Inc." <pyansys.core@ansys.com>
7
- Maintainer: ANSYS, Inc.
8
- Maintainer-email: "ANSYS, Inc." <pyansys.core@ansys.com>
5
+ Author: Synopsys, Inc. and ANSYS, Inc.
6
+ Author-email: "Synopsys, Inc. and ANSYS, Inc." <pyansys-core@synopsys.com>
7
+ Maintainer: Synopsys, Inc. and ANSYS, Inc.
8
+ Maintainer-email: "Synopsys, Inc. and ANSYS, Inc." <pyansys-core@synopsys.com>
9
9
  License-Expression: MIT
10
10
  Project-URL: Source, https://github.com/ansys/pre-commit-hooks/
11
11
  Project-URL: Issues, https://github.com/ansys/pre-commit-hooks/issues
@@ -24,15 +24,15 @@ Classifier: Programming Language :: Python :: 3.13
24
24
  Requires-Python: >=3.10,<4
25
25
  Description-Content-Type: text/x-rst
26
26
  License-File: LICENSE
27
- Requires-Dist: GitPython==3.1.49
27
+ Requires-Dist: GitPython==3.1.50
28
28
  Requires-Dist: importlib-metadata==9.0.0
29
29
  Requires-Dist: Jinja2==3.1.6
30
30
  Requires-Dist: reuse==6.2.0
31
- Requires-Dist: requests==2.33.1
31
+ Requires-Dist: requests==2.34.2
32
32
  Requires-Dist: semver==3.0.4
33
33
  Requires-Dist: toml==0.10.2
34
34
  Provides-Extra: doc
35
- Requires-Dist: ansys-sphinx-theme[autoapi]==1.7.2; extra == "doc"
35
+ Requires-Dist: ansys-sphinx-theme[autoapi]==1.8.2; extra == "doc"
36
36
  Requires-Dist: numpydoc==1.10.0; extra == "doc"
37
37
  Requires-Dist: sphinx==8.2.3; extra == "doc"
38
38
  Requires-Dist: sphinx-autodoc-typehints==3.1.0; extra == "doc"
@@ -1,10 +1,10 @@
1
1
  [build-system]
2
2
  requires = [
3
3
  "setuptools>=65.5.1",
4
- "GitPython==3.1.49",
4
+ "GitPython==3.1.50",
5
5
  "Jinja2==3.1.6",
6
6
  "reuse==6.2.0",
7
- "requests==2.33.1",
7
+ "requests==2.34.2",
8
8
  "semver==3.0.4",
9
9
  "toml==0.10.2",
10
10
  "wheel",
@@ -17,14 +17,14 @@ dynamic = ["version", "classifiers", "description", "readme", "scripts"]
17
17
  requires-python = ">=3.10,<4"
18
18
  license = "MIT"
19
19
  license-files = ["LICENSE"]
20
- authors = [{ name = "ANSYS, Inc.", email = "pyansys.core@ansys.com" }]
21
- maintainers = [{ name = "ANSYS, Inc.", email = "pyansys.core@ansys.com" }]
20
+ authors = [{ name = "Synopsys, Inc. and ANSYS, Inc.", email = "pyansys-core@synopsys.com" }]
21
+ maintainers = [{ name = "Synopsys, Inc. and ANSYS, Inc.", email = "pyansys-core@synopsys.com" }]
22
22
  dependencies = [
23
- "GitPython==3.1.49",
23
+ "GitPython==3.1.50",
24
24
  "importlib-metadata==9.0.0",
25
25
  "Jinja2==3.1.6",
26
26
  "reuse==6.2.0",
27
- "requests==2.33.1",
27
+ "requests==2.34.2",
28
28
  "semver==3.0.4",
29
29
  "toml==0.10.2",
30
30
  ]
@@ -47,7 +47,7 @@ classifiers = {file = """
47
47
 
48
48
  [project.optional-dependencies]
49
49
  doc = [
50
- "ansys-sphinx-theme[autoapi]==1.7.2",
50
+ "ansys-sphinx-theme[autoapi]==1.8.2",
51
51
  "numpydoc==1.10.0",
52
52
  "sphinx==8.2.3",
53
53
  "sphinx-autodoc-typehints==3.1.0",
@@ -22,10 +22,10 @@ setup(
22
22
  description=description,
23
23
  long_description=long_description,
24
24
  license="MIT",
25
- author="ANSYS, Inc.",
26
- author_email="pyansys.core@ansys.com",
27
- maintainer="ANSYS, Inc.",
28
- maintainer_email="pyansys.core@ansys.com",
25
+ author="Synopsys, Inc. and ANSYS, Inc.",
26
+ author_email="pyansys-core@synopsys.com",
27
+ maintainer="Synopsys, Inc. and ANSYS, Inc.",
28
+ maintainer_email="pyansys-core@synopsys.com",
29
29
  classifiers=[
30
30
  "Development Status :: 4 - Beta",
31
31
  "Intended Audience :: Science/Research",
@@ -39,17 +39,17 @@ setup(
39
39
  url="https://github.com/ansys/pre-commit-hooks",
40
40
  python_requires=">=3.10,<4",
41
41
  install_requires=[
42
- "GitPython==3.1.49",
42
+ "GitPython==3.1.50",
43
43
  "importlib-metadata==9.0.0",
44
44
  "Jinja2==3.1.6",
45
45
  "reuse==6.2.0",
46
- "requests==2.33.1",
46
+ "requests==2.34.2",
47
47
  "semver==3.0.4",
48
48
  "toml==0.10.2",
49
49
  ],
50
50
  extras_require={
51
51
  "doc": [
52
- "ansys-sphinx-theme[autoapi]==1.7.2",
52
+ "ansys-sphinx-theme[autoapi]==1.8.2",
53
53
  "numpydoc==1.10.0",
54
54
  "sphinx==8.2.3",
55
55
  "sphinx-autodoc-typehints==3.1.0",
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -30,6 +30,7 @@ import argparse
30
30
  from datetime import date as dt
31
31
  import filecmp
32
32
  import io
33
+ from logging import Logger
33
34
  from pathlib import Path
34
35
  import re
35
36
  import shutil
@@ -37,16 +38,76 @@ import sys
37
38
  from tempfile import NamedTemporaryFile
38
39
  from typing import IO, Union
39
40
 
41
+ logger = Logger(__name__)
42
+
40
43
  DEFAULT_TEMPLATE = "ansys"
41
44
  """Default template to use for license headers."""
42
- DEFAULT_COPYRIGHT = "ANSYS, Inc. and/or its affiliates."
45
+ DEFAULT_COPYRIGHT = "Synopsys, Inc. and ANSYS, Inc. All rights reserved."
43
46
  """Default copyright line for license headers."""
44
47
  DEFAULT_LICENSE = "MIT"
45
48
  """Default license for headers."""
46
49
  DEFAULT_START_YEAR = dt.today().year
47
- """Default start year for license headers."""
50
+ """Fallback start year used when git history is unavailable."""
48
51
  YEAR_REGEX = r"(\d{4}) - (\d{4})|\d{4}"
49
52
  """Year regex to match year or year range in files."""
53
+ HEADER_PEEK_BYTES = 1024
54
+ """Number of bytes read from the top of a file to check for an existing license header.
55
+
56
+ License headers always appear at the very start of a file and are at most a few hundred
57
+ bytes, so reading only this many bytes avoids loading the full file for every checked file."""
58
+
59
+
60
+ def get_start_year_from_git(git_repo) -> int:
61
+ """Return the year of the first (oldest) commit in the git repository.
62
+
63
+ Uses ``git log --reverse`` to find the oldest commit and extracts its year.
64
+ When the repository is a shallow clone (e.g. ``fetch-depth: 1`` in CI/CD),
65
+ attempts ``git fetch --unshallow`` to retrieve the full history before
66
+ reading the first commit year. If unshallowing fails (no network access,
67
+ no remote configured, etc.) the function continues with the limited history
68
+ that is locally available.
69
+ Falls back to :data:`DEFAULT_START_YEAR` (the current year) when the
70
+ repository has no commits yet.
71
+
72
+ Parameters
73
+ ----------
74
+ git_repo: git.Repo
75
+ The git repository object.
76
+
77
+ Returns
78
+ -------
79
+ int
80
+ The four-digit year of the first commit, or the current year if the
81
+ repository has no commits.
82
+ """
83
+ # Detect shallow clones produced by CI/CD checkouts with fetch-depth: 1.
84
+ # In that case git log --reverse only sees the single fetched commit, not
85
+ # the true first commit in history.
86
+ try:
87
+ is_shallow = git_repo.git.rev_parse("--is-shallow-repository").strip() == "true"
88
+ except Exception:
89
+ # Older git versions (<2.15) do not support --is-shallow-repository;
90
+ # fall back to checking for the presence of the .git/shallow file.
91
+ is_shallow = (Path(git_repo.git_dir) / "shallow").is_file()
92
+
93
+ if is_shallow:
94
+ # Attempt to retrieve the full history so the real first commit is visible.
95
+ try:
96
+ git_repo.git.fetch("--unshallow")
97
+ except Exception as e:
98
+ # Unshallow can fail when there is no network, no remote is configured,
99
+ # or the server does not support it. Continue with whatever history is
100
+ # available locally.
101
+ logger.debug(f"Failure during unshallow: {e}")
102
+ logger.debug("Continuing with limited history available in shallow clone.")
103
+
104
+ # --reverse makes the oldest commit appear first; %ad is the author date
105
+ first_year_str = (
106
+ git_repo.git.log("--reverse", "--format=%ad", "--date=format:%Y").split("\n")[0].strip()
107
+ )
108
+ if first_year_str and first_year_str.isdigit():
109
+ return int(first_year_str)
110
+ return DEFAULT_START_YEAR
50
111
 
51
112
 
52
113
  def set_lint_args(parser: argparse.ArgumentParser) -> argparse.Namespace:
@@ -90,8 +151,11 @@ def set_lint_args(parser: argparse.ArgumentParser) -> argparse.Namespace:
90
151
  parser.add_argument(
91
152
  "--start_year",
92
153
  type=str,
93
- help="Start year for copyright line in headers.",
94
- default=DEFAULT_START_YEAR,
154
+ help=(
155
+ "Start year for copyright line in headers. "
156
+ "When omitted the year of the first git commit is used automatically."
157
+ ),
158
+ default=None,
95
159
  )
96
160
  # Ignore license check by default is False when action='store_true'
97
161
  parser.add_argument("--ignore_license_check", action="store_true")
@@ -371,7 +435,8 @@ def _has_current_header(file_path: str, copyright: list, years: str) -> bool:
371
435
  file_path: str
372
436
  Path to the file to check.
373
437
  copyright: list
374
- List containing the copyright string. For example, ["ANSYS, Inc. and/or its affiliates."].
438
+ List containing the copyright string. For example,
439
+ ["Synopsys, Inc. and ANSYS, Inc. All rights reserved."].
375
440
  years: str
376
441
  The expected year span. For example, "2023 - 2026" or "2026".
377
442
 
@@ -383,7 +448,7 @@ def _has_current_header(file_path: str, copyright: list, years: str) -> bool:
383
448
  """
384
449
  try:
385
450
  with Path(file_path).open(encoding="utf-8", errors="ignore") as f:
386
- head = f.read(1024)
451
+ head = f.read(HEADER_PEEK_BYTES)
387
452
  except OSError:
388
453
  return False
389
454
 
@@ -410,7 +475,7 @@ def _file_has_license(file_path: str, license: str) -> bool:
410
475
  """
411
476
  try:
412
477
  with Path(file_path).open(encoding="utf-8", errors="ignore") as f:
413
- head = f.read(1024)
478
+ head = f.read(HEADER_PEEK_BYTES)
414
479
  except OSError:
415
480
  return False
416
481
 
@@ -511,9 +576,16 @@ def non_recursive_file_check(
511
576
  add_header(copyright, license, years, file, template, commented, sys.stdout)
512
577
  else:
513
578
  # Check whether the existing SPDX-License-Identifier differs from the
514
- # requested license. If so, strip the old header and write a fresh one
515
- # so that the old identifier is fully replaced rather than merged.
516
- if not existing_license_matches:
579
+ # requested license, or whether the copyright holder phrase has changed.
580
+ # In either case, strip the old header and write a fresh one so that the
581
+ # old lines are fully replaced rather than merged by REUSE's annotate.
582
+ try:
583
+ with Path(file).open(encoding="utf-8", errors="ignore") as _f:
584
+ _head = _f.read(HEADER_PEEK_BYTES)
585
+ except OSError:
586
+ _head = ""
587
+ copyright_holder_changed = copyright[0] not in _head
588
+ if not existing_license_matches or copyright_holder_changed:
517
589
  before_hook = NamedTemporaryFile(mode="w", delete=False).name
518
590
  shutil.copyfile(file, before_hook)
519
591
  _strip_reuse_header(file)
@@ -585,7 +657,8 @@ def update_header(
585
657
  file: str
586
658
  The file whose header is being updated.
587
659
  copyright: str
588
- The copyright string of the header. For example, "ANSYS, Inc. and/or its affiliates."
660
+ The copyright string of the header. For example,
661
+ "Synopsys, Inc. and ANSYS, Inc. All rights reserved."
589
662
  license: str
590
663
  The license of the header. For example, "MIT".
591
664
  years: str
@@ -672,7 +745,7 @@ def add_header(
672
745
  ----------
673
746
  copyright: str
674
747
  The copyright line for the license header. For example,
675
- "ANSYS, Inc. and/or its affiliates."
748
+ "Synopsys, Inc. and ANSYS, Inc. All rights reserved."
676
749
  license: str
677
750
  The license for the license header. For example, "MIT".
678
751
  years: str
@@ -942,17 +1015,6 @@ def main():
942
1015
  # Set changed_headers to zero by default
943
1016
  changed_headers = 0
944
1017
 
945
- # Check start_year is valid
946
- if str(args.start_year).isdigit():
947
- # Check the start year is not later than the current year
948
- if int(args.start_year) > dt.today().year:
949
- raise Exception("Please provide a start year less than or equal to the current year.")
950
- # Check the start year isn't earlier than when computers were created :)
951
- elif int(args.start_year) < 1942:
952
- raise Exception("Please provide a start year greater than or equal to 1942.")
953
- else:
954
- raise Exception("Please ensure the start year is a number.")
955
-
956
1018
  import git
957
1019
 
958
1020
  # Get root directory of the git repository.
@@ -960,6 +1022,27 @@ def main():
960
1022
  # Get the root of the git repository and fix the line separators
961
1023
  git_root = Path(git_repo.git.rev_parse("--show-toplevel"))
962
1024
 
1025
+ # Determine the start year.
1026
+ # When --start_year is explicitly provided, validate and use that value.
1027
+ # Otherwise, auto-detect from the year of the first git commit so that the
1028
+ # original copyright year is never accidentally erased.
1029
+ if args.start_year is not None:
1030
+ if str(args.start_year).isdigit():
1031
+ # Check the start year is not later than the current year
1032
+ if int(args.start_year) > dt.today().year:
1033
+ raise Exception(
1034
+ "Please provide a start year less than or equal to the current year."
1035
+ )
1036
+ # Check the start year isn't earlier than when computers were created :)
1037
+ elif int(args.start_year) < 1942:
1038
+ raise Exception("Please provide a start year greater than or equal to 1942.")
1039
+ else:
1040
+ raise Exception("Please ensure the start year is a number.")
1041
+ start_year = int(args.start_year)
1042
+ else:
1043
+ # Auto-detect: use the year of the repository's first commit
1044
+ start_year = get_start_year_from_git(git_repo)
1045
+
963
1046
  # Create dictionary containing the committed files, custom copyright,
964
1047
  # template, license, changed_headers, year, and git_repo
965
1048
  values = {
@@ -967,7 +1050,7 @@ def main():
967
1050
  "copyright": args.custom_copyright,
968
1051
  "template": args.custom_template,
969
1052
  "license": args.custom_license,
970
- "start_year": int(args.start_year),
1053
+ "start_year": start_year,
971
1054
  "current_year": dt.today().year,
972
1055
  "git_repo": git_repo,
973
1056
  }
@@ -187,7 +187,7 @@
187
187
  same "printed page" as the copyright notice for easier
188
188
  identification within third-party archives.
189
189
 
190
- Copyright {{ year_span }} ANSYS, Inc. and/or its affiliates.
190
+ Copyright {{ year_span }} Synopsys, Inc. and ANSYS, Inc. All rights reserved.
191
191
 
192
192
  Licensed under the Apache License, Version 2.0 (the "License");
193
193
  you may not use this file except in compliance with the License.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) {{ year_span }} ANSYS, Inc. and/or its affiliates.
3
+ Copyright (c) {{ year_span }} Synopsys, Inc. and ANSYS, Inc. All rights reserved.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -37,10 +37,10 @@ HOOK_PATH = pathlib.Path(__file__).parent.resolve()
37
37
  LICENSES_JSON = HOOK_PATH / "assets" / "licenses.json"
38
38
  """JSON file containing licenses information."""
39
39
 
40
- DEFAULT_AUTHOR_MAINT_NAME = "ANSYS, Inc."
40
+ DEFAULT_AUTHOR_MAINT_NAME = "Synopsys, Inc. and ANSYS, Inc."
41
41
  """Default name of project authors and maintainers."""
42
42
 
43
- DEFAULT_AUTHOR_MAINT_EMAIL = "pyansys.core@ansys.com"
43
+ DEFAULT_AUTHOR_MAINT_EMAIL = "pyansys-core@synopsys.com"
44
44
  """Default email of project authors and maintainers."""
45
45
 
46
46
  DEFAULT_START_YEAR = dt.today().year
@@ -9,4 +9,4 @@
9
9
  # submit a request.
10
10
  #
11
11
  #
12
- ANSYS, Inc.
12
+ Synopsys, Inc. and ANSYS, Inc.
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) {{ year_span }} ANSYS, Inc. and/or its affiliates.
3
+ Copyright (c) {{ year_span }} Synopsys, Inc. and ANSYS, Inc. All rights reserved.
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of
6
6
  this software and associated documentation files (the "Software"), to deal in
@@ -1,11 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ansys-pre-commit-hooks
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Home-page: https://github.com/ansys/pre-commit-hooks
5
- Author: ANSYS, Inc.
6
- Author-email: "ANSYS, Inc." <pyansys.core@ansys.com>
7
- Maintainer: ANSYS, Inc.
8
- Maintainer-email: "ANSYS, Inc." <pyansys.core@ansys.com>
5
+ Author: Synopsys, Inc. and ANSYS, Inc.
6
+ Author-email: "Synopsys, Inc. and ANSYS, Inc." <pyansys-core@synopsys.com>
7
+ Maintainer: Synopsys, Inc. and ANSYS, Inc.
8
+ Maintainer-email: "Synopsys, Inc. and ANSYS, Inc." <pyansys-core@synopsys.com>
9
9
  License-Expression: MIT
10
10
  Project-URL: Source, https://github.com/ansys/pre-commit-hooks/
11
11
  Project-URL: Issues, https://github.com/ansys/pre-commit-hooks/issues
@@ -24,15 +24,15 @@ Classifier: Programming Language :: Python :: 3.13
24
24
  Requires-Python: >=3.10,<4
25
25
  Description-Content-Type: text/x-rst
26
26
  License-File: LICENSE
27
- Requires-Dist: GitPython==3.1.49
27
+ Requires-Dist: GitPython==3.1.50
28
28
  Requires-Dist: importlib-metadata==9.0.0
29
29
  Requires-Dist: Jinja2==3.1.6
30
30
  Requires-Dist: reuse==6.2.0
31
- Requires-Dist: requests==2.33.1
31
+ Requires-Dist: requests==2.34.2
32
32
  Requires-Dist: semver==3.0.4
33
33
  Requires-Dist: toml==0.10.2
34
34
  Provides-Extra: doc
35
- Requires-Dist: ansys-sphinx-theme[autoapi]==1.7.2; extra == "doc"
35
+ Requires-Dist: ansys-sphinx-theme[autoapi]==1.8.2; extra == "doc"
36
36
  Requires-Dist: numpydoc==1.10.0; extra == "doc"
37
37
  Requires-Dist: sphinx==8.2.3; extra == "doc"
38
38
  Requires-Dist: sphinx-autodoc-typehints==3.1.0; extra == "doc"
@@ -1,13 +1,13 @@
1
- GitPython==3.1.49
1
+ GitPython==3.1.50
2
2
  importlib-metadata==9.0.0
3
3
  Jinja2==3.1.6
4
4
  reuse==6.2.0
5
- requests==2.33.1
5
+ requests==2.34.2
6
6
  semver==3.0.4
7
7
  toml==0.10.2
8
8
 
9
9
  [doc]
10
- ansys-sphinx-theme[autoapi]==1.7.2
10
+ ansys-sphinx-theme[autoapi]==1.8.2
11
11
  numpydoc==1.10.0
12
12
  sphinx==8.2.3
13
13
  sphinx-autodoc-typehints==3.1.0
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -37,7 +37,7 @@ import ansys.pre_commit_hooks.add_license_headers as hook
37
37
  git_repo = git.Repo(Path.cwd(), search_parent_directories=True)
38
38
  REPO_PATH = git_repo.git.rev_parse("--show-toplevel")
39
39
  START_YEAR = "2023"
40
- DEFAULT_COPYRIGHT = "ANSYS, Inc. and/or its affiliates."
40
+ DEFAULT_COPYRIGHT = "Synopsys, Inc. and ANSYS, Inc. All rights reserved."
41
41
 
42
42
 
43
43
  def set_up_repo(tmp_path, template_path, template_name, license_path, license_name):
@@ -129,7 +129,7 @@ def check_ansys_header(file_name):
129
129
  for line in file:
130
130
  count += 1
131
131
  if count == 1:
132
- assert "ANSYS, Inc. and/or its affiliates" in line
132
+ assert "Synopsys, Inc. and ANSYS, Inc." in line
133
133
  if count == 2:
134
134
  assert "MIT" in line
135
135
  if count == 5:
@@ -189,7 +189,7 @@ def test_start_year_same_as_current(tmp_path: pytest.TempPathFactory):
189
189
  count += 1
190
190
  # Assert the copyright line's time range is from 2023 to the current year
191
191
  if count == 1:
192
- assert f"Copyright (C) {dt.today().year} ANSYS, Inc." in line
192
+ assert f"Copyright (C) {dt.today().year} Synopsys, Inc. and ANSYS, Inc." in line
193
193
  if count > 1:
194
194
  break
195
195
 
@@ -356,7 +356,7 @@ def test_no_license_check(tmp_path: pytest.TempPathFactory):
356
356
  count += 1
357
357
  # Assert that only the copyright line is in the file
358
358
  if count == 1:
359
- assert "ANSYS, Inc. and/or its affiliates" in line
359
+ assert "Synopsys, Inc. and ANSYS, Inc." in line
360
360
  if count == 2:
361
361
  assert "MIT" not in line
362
362
  if count == 5:
@@ -735,6 +735,81 @@ def test_license_year_update(tmp_path: pytest.TempPathFactory):
735
735
  os.chdir(REPO_PATH)
736
736
 
737
737
 
738
+ @pytest.mark.add_license_headers
739
+ def test_get_start_year_from_git_new_repo(tmp_path: pytest.TempPathFactory):
740
+ """Test that get_start_year_from_git returns the current year for a brand-new repo."""
741
+ os.chdir(tmp_path)
742
+ repo = init_repo(tmp_path)
743
+ year = hook.get_start_year_from_git(repo)
744
+ assert year == dt.today().year
745
+ os.chdir(REPO_PATH)
746
+
747
+
748
+ @pytest.mark.add_license_headers
749
+ def test_get_start_year_from_git_detects_first_commit_year(tmp_path: pytest.TempPathFactory):
750
+ """Test that get_start_year_from_git returns the year of the *first* commit.
751
+
752
+ Creates a repo with a backdated initial commit so the result is deterministic
753
+ regardless of when the test suite is run.
754
+ """
755
+ from datetime import datetime
756
+
757
+ os.chdir(tmp_path)
758
+ git.Repo.init(tmp_path)
759
+ repo = git.Repo(tmp_path)
760
+
761
+ # Back-dated initial commit — gitpython requires timezone-aware datetimes
762
+ from datetime import timezone
763
+
764
+ old_date = datetime(2020, 6, 15, tzinfo=timezone.utc)
765
+ readme = tmp_path / "README.md"
766
+ readme.write_text("initial")
767
+ repo.index.add([str(readme)])
768
+ repo.index.commit("initial commit", author_date=old_date, commit_date=old_date)
769
+
770
+ # A more recent commit — must not affect the result
771
+ readme.write_text("updated")
772
+ repo.index.add([str(readme)])
773
+ repo.index.commit("recent commit")
774
+
775
+ year = hook.get_start_year_from_git(repo)
776
+ assert year == 2020
777
+
778
+ os.chdir(REPO_PATH)
779
+
780
+
781
+ @pytest.mark.add_license_headers
782
+ def test_start_year_autodetected_from_git(tmp_path: pytest.TempPathFactory):
783
+ """End-to-end test: when --start_year is omitted the hook uses the first commit year.
784
+
785
+ A fresh repo is initialised so the first commit year equals the current year.
786
+ Running the hook without ``--start_year`` should therefore produce a copyright
787
+ line that contains only the current year (no range).
788
+ """
789
+ template_name = "ansys.jinja2"
790
+ license_name = "MIT.txt"
791
+ template_path = Path(REPO_PATH) / ".reuse" / "templates" / template_name
792
+ license_path = Path(REPO_PATH) / "LICENSES" / license_name
793
+
794
+ os.chdir(tmp_path)
795
+ repo, tmp_file = set_up_repo(tmp_path, template_path, template_name, license_path, license_name)
796
+
797
+ # Run the hook with NO --start_year argument
798
+ assert add_argv_run(repo, tmp_file, [tmp_file]) == 1
799
+
800
+ with open(tmp_file, "r") as f:
801
+ first_line = f.readline()
802
+
803
+ # New repo: first commit is in the current year, so the copyright should
804
+ # contain only the current year (no multi-year range).
805
+ assert (
806
+ f"Copyright (C) {dt.today().year} Synopsys, Inc. and ANSYS, Inc. All rights reserved."
807
+ in first_line
808
+ )
809
+
810
+ os.chdir(REPO_PATH)
811
+
812
+
738
813
  @pytest.mark.add_license_headers
739
814
  def test_date_update(tmp_path: pytest.TempPathFactory):
740
815
  """Test the date is correctly updated in the license header."""
@@ -755,11 +830,17 @@ def test_date_update(tmp_path: pytest.TempPathFactory):
755
830
  years = ["2022", "2023", str(dt.today().year)]
756
831
 
757
832
  # Check the copyright line has "2023 - {current_year}", "2022 - {current_year}"
758
- # and "{current_year}"
833
+ # and "{current_year}" (contained within the preserved range)
759
834
  for year in years:
760
835
  custom_args = [tmp_file, f"--start_year={year}"]
761
836
  # Git add tmp_file and run hook with custom arguments
762
- assert add_argv_run(repo, tmp_file, custom_args) == 1
837
+ result = add_argv_run(repo, tmp_file, custom_args)
838
+ # When the start year differs from the current year the hook must update the
839
+ # header (return code 1). When start_year equals the current year and files
840
+ # already have the current year as their end year (set during earlier
841
+ # iterations), the hook preserves the existing range and returns 0.
842
+ if str(year) != str(dt.today().year):
843
+ assert result == 1
763
844
  # Check the license year is correctly updated
764
845
  check_license_year(tmp_file, DEFAULT_COPYRIGHT, year, str(dt.today().year))
765
846
  # Add file with updated header
@@ -868,7 +949,7 @@ def check_apache_header(file_name):
868
949
  """Check file contains all Apache-2.0 copyright and license header components."""
869
950
  with open(file_name, "r", encoding="utf8") as file:
870
951
  content = file.read()
871
- assert "ANSYS, Inc. and/or its affiliates" in content
952
+ assert "Synopsys, Inc. and ANSYS, Inc." in content
872
953
  assert "Apache-2.0" in content
873
954
  assert "Apache License, Version 2.0" in content
874
955
  assert "http://www.apache.org/licenses/LICENSE-2.0" in content
@@ -1047,7 +1128,7 @@ def test_apache_license_year_update_preserves_boilerplate(tmp_path):
1047
1128
 
1048
1129
  initial_content = license_file.read_text(encoding="utf-8")
1049
1130
  assert "January 2004" in initial_content
1050
- assert "Copyright 2023 ANSYS" in initial_content
1131
+ assert "Copyright 2023 Synopsys, Inc. and ANSYS, Inc." in initial_content
1051
1132
 
1052
1133
  # Simulate a year-span update (e.g., a new calendar year)
1053
1134
  hook.update_license_file(license_file, "2023 - 2026", "Apache-2.0")
@@ -1056,7 +1137,7 @@ def test_apache_license_year_update_preserves_boilerplate(tmp_path):
1056
1137
  assert (
1057
1138
  "January 2004" in updated_content
1058
1139
  ), "The Apache boilerplate 'January 2004' must not be overwritten by the year update"
1059
- assert "Copyright 2023 - 2026 ANSYS" in updated_content
1140
+ assert "Copyright 2023 - 2026 Synopsys, Inc. and ANSYS, Inc." in updated_content
1060
1141
 
1061
1142
 
1062
1143
  @pytest.mark.add_license_headers
@@ -1091,3 +1172,55 @@ def test_line_endings(tmp_path: pytest.TempPathFactory):
1091
1172
  assert license_line_endings_before == get_line_endings(tmp_license)
1092
1173
 
1093
1174
  os.chdir(REPO_PATH)
1175
+
1176
+
1177
+ @pytest.mark.add_license_headers
1178
+ def test_copyright_holder_change_replaces_not_duplicates(tmp_path: pytest.TempPathFactory):
1179
+ """Test that changing the copyright holder replaces the old line instead of adding a new one.
1180
+
1181
+ Regression test: when a file already has a valid header with the *old* copyright phrase
1182
+ (e.g. 'ANSYS, Inc. and/or its affiliates.') and the hook is re-run with a new phrase
1183
+ (e.g. 'Synopsys, Inc. and ANSYS, Inc. All rights reserved.'), the header must be
1184
+ fully replaced — not duplicated — so only one copyright line exists in the file.
1185
+ """
1186
+ template_name = "ansys.jinja2"
1187
+ license_name = "MIT.txt"
1188
+ template_path = Path(REPO_PATH) / ".reuse" / "templates" / template_name
1189
+ license_path = Path(REPO_PATH) / "LICENSES" / license_name
1190
+
1191
+ repo, tmp_file = set_up_repo(tmp_path, template_path, template_name, license_path, license_name)
1192
+
1193
+ old_copyright = "ANSYS, Inc. and/or its affiliates."
1194
+ new_copyright = "Synopsys, Inc. and ANSYS, Inc. All rights reserved."
1195
+
1196
+ # First run: add header with the *old* copyright phrase
1197
+ old_args = [tmp_file, f"--custom_copyright={old_copyright}"]
1198
+ assert add_argv_run(repo, tmp_file, old_args) == 1
1199
+ repo.index.add([tmp_file])
1200
+
1201
+ with open(tmp_file, "r", encoding="utf-8") as f:
1202
+ content_after_first_run = f.read()
1203
+ assert old_copyright in content_after_first_run
1204
+ assert new_copyright not in content_after_first_run
1205
+
1206
+ # Second run: re-run with the new copyright phrase (default)
1207
+ new_args = [tmp_file, f"--custom_copyright={new_copyright}"]
1208
+ assert add_argv_run(repo, tmp_file, new_args) == 1
1209
+
1210
+ with open(tmp_file, "r", encoding="utf-8") as f:
1211
+ content_after_second_run = f.read()
1212
+
1213
+ # The new phrase must be present
1214
+ assert new_copyright in content_after_second_run
1215
+ # The old phrase must be gone
1216
+ assert old_copyright not in content_after_second_run
1217
+ # Exactly one copyright line (no duplicates)
1218
+ copyright_line_count = sum(
1219
+ 1 for line in content_after_second_run.splitlines() if "Copyright" in line
1220
+ )
1221
+ assert copyright_line_count == 1, (
1222
+ f"Expected exactly 1 copyright line, found {copyright_line_count}:\n"
1223
+ f"{content_after_second_run}"
1224
+ )
1225
+
1226
+ os.chdir(REPO_PATH)
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2023 - 2026 ANSYS, Inc. and/or its affiliates.
1
+ # Copyright (C) 2023 - 2026 Synopsys, Inc. and ANSYS, Inc. All rights reserved.
2
2
  # SPDX-License-Identifier: MIT
3
3
  #
4
4
  #
@@ -96,8 +96,8 @@ def run_main(custom_args):
96
96
  @pytest.mark.tech_review
97
97
  def test_pyproject_toml(tmp_path: pytest.TempPathFactory):
98
98
  """Test pyproject.toml retrieves all information."""
99
- author_maint_name = "ANSYS, Inc."
100
- author_maint_email = "pyansys.core@ansys.com"
99
+ author_maint_name = "Synopsys, Inc. and ANSYS, Inc."
100
+ author_maint_email = "pyansys-core@synopsys.com"
101
101
  is_compliant = True
102
102
  non_compliant_name = False
103
103
 
@@ -282,7 +282,7 @@ def test_bad_author_maint_name_email(tmp_path: pytest.TempPathFactory, capsys):
282
282
  setup_repo(tmp_path)
283
283
 
284
284
  # Remove name and email from author and maintainer in pyproject.toml
285
- search = '{name = "ANSYS, Inc.", email = "pyansys.core@ansys.com"},'
285
+ search = '{name = "Synopsys, Inc. and ANSYS, Inc.", email = "pyansys-core@synopsys.com"},'
286
286
  replace = "{},"
287
287
  replace_line(tmp_path, "pyproject.toml", search, replace)
288
288