Skip to content

Add licensepy (license header formatting) to checks-superstaq#983

Merged
bharat-thotakura merged 59 commits into
mainfrom
license-check
Dec 6, 2025
Merged

Add licensepy (license header formatting) to checks-superstaq#983
bharat-thotakura merged 59 commits into
mainfrom
license-check

Conversation

@natibek

@natibek natibek commented Jul 8, 2024

Copy link
Copy Markdown
Contributor

Update: #983 (comment)

This PR adds licensepy to checks-superstaq, enabling license header formatting as configured in the project's pyproject.toml. This can be toggled in checks/all_.py or can be run directly via checks/license_header_.py. This PR also adds this check to the CI for enforcement moving forward


Outdated:

There are inconsistencies in when license headers are added to source code (and whether the added ones are the same). This PR solves this with a checker that checks if the source code has a license header and if it does, whether it is the correct one. It accounts for shebang lines, comments at the beginning of files unrelated to license headers, and pylint and mypy disable lines. This check has been added to all_.py._

  • Like the other checks, it can be called with ./checks/license_header_format_.py from the root directory.
  • -i can be used to perform an incremental check. This is not enabled by default.
  • A specific file can be passed as an argument and just that will be checked. By default, it checks all the files matching *.py.
  • The --no-header flag is used to handle only cases where no header is found. By default, it is False.
  • The --bad-header flag is used to handle only cases where an incorrect header is found. By default, it is False.
  • The --apply flag is used to fix the header problems. By default, it only checks if the headers are correct and does not make any fixes.
    • It will remove bad headers and replace them with the correct ones.
    • If no license header is found, it will add one.

@natibek natibek requested review from richrines1 and vtomole July 8, 2024 17:38
@natibek natibek changed the title License header checker and adder License header formatting Jul 11, 2024
@richrines1 richrines1 self-assigned this Aug 2, 2024

@richrines1 richrines1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you for this!! added some inline suggestions. a few comments more generally:

  • there should be no references to "server" here. Any dynamic/repo-specific behavior should be handled via arguments or the config file - we would like checks in checks-superstaq to generalize outside of our repos as much as possible (within reason)
  • once we correct outdated headers once, it seems like we shouldn't need to keep checking them? in which case maybe the "outdated" functionality doesn't need to live in in this script - we can save the code you use to make these initial corrections somewhere internally, and then use this script to check headers from here on out
  • if we still need to explicitly check for the string "Infleqtion" in places, maybe we could add a "licensee" value to the config file in addition to the header, instead of hard coding it?
  • similarly, i can maybe see why it's unavoidable but i feel like the hard-coded "apache" checks somewhat defeat the purposes of saving the header in the config. do you think there's an easy way to check if the headers are ~the same, up to licensee/year? maybe we could allow the header in pyproject.toml to include {YEAR} and {LICENSEE} tags, which we could convert to wildcards when comparing against existing licenses

also fwiw it's also ok if this script doesn't handle every possible case perfectly - if it gets confused it can always just throw an error saying to fix the headers manually :)

"""
)
parser.add_argument(
"--apply", action="store_true", help="Add the license header to files.", default=False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to set default=False with action="store_true"

Suggested change
"--apply", action="store_true", help="Add the license header to files.", default=False
"--apply", action="store_true", help="Add the license header to files."

(ditto below)

license_header = ""
exceptions = ["# pylint:", "#!/", "# mypy:"]

with open(file, "r+") as f:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we need the "+" here? if not:

Suggested change
with open(file, "r+") as f:
with open(file, "r") as f:

Comment on lines +34 to +37
try:
data: dict[str, Any] = tomlkit.parse(Path("pyproject.toml").read_text())
expected_license_header = str(data["tool"]["license_header_format"]["license_header"])
in_server = "Apache" not in expected_license_header

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should put this in a function instead executing it globally

Comment on lines +39 to +42
raise KeyError(
"Under [tool.license_header_format] add a license_header field with the license\
heder that should be added to source code files in the repository."
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need to raise an error in this case, we can just have an info message saying that no license header was found and then return as if it succeeded

Comment thread checks-superstaq/checks_superstaq/license_header_format_.py Outdated
Comment on lines +81 to +84
return f"""
Beginning at line: {self.start_line_num}
Ending at line : {self.end_line_num}\n
{self.license_header}\n"""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit

Suggested change
return f"""
Beginning at line: {self.start_line_num}
Ending at line : {self.end_line_num}\n
{self.license_header}\n"""
return (
f"Beginning at line: {self.start_line_num}\n"
f"Ending at line : {self.end_line_num}\n\n"
f"{self.license_header}\n"
)

@natibek

natibek commented Aug 5, 2024

Copy link
Copy Markdown
Contributor Author
  • once we correct outdated headers once, it seems like we shouldn't need to keep checking them? in which case maybe the "outdated" functionality doesn't need to live in in this script - we can save the code you use to make these initial corrections somewhere internally, and then use this script to check headers from here on out

That makes sense. We can also keep it but change the logic a bit. After the initial fix, instead of checking for ColdQuanta in the license header, we can check if it belongs to the licensee but is a different license. This can catch cases of changing the license provider.

  • similarly, i can maybe see why it's unavoidable but i feel like the hard-coded "apache" checks somewhat defeat the purposes of saving the header in the config. do you think there's an easy way to check if the headers are ~the same, up to licensee/year? maybe we could allow the header in pyproject.toml to include {YEAR} and {LICENSEE} tags, which we could convert to wildcards when comparing against existing licenses

I added a few more fields to replace the hard-coded variables. The cirq license header check pylint plugin does something similar. However, apache 2.0 licenses seem to have 2 different formattings from what I have seen in the license headers and that would mess with the matching if we use the wild card approach.

and license_header.start_line_num <= line_num + 1 < license_header.end_line_num
):
if line[-2] == ",":
prepend += line[:-1] + f" 2024 {licensee}.\n"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

silly legal q: will we need to update the year in every file on 1/1/2025? or do they stay the same until if/when we update the file?

if the former we might want to replace 2024 with e.g. datetime.datetime.now().year. if the latter maybe we want to make the last two digits wildcards?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will we need to update the year in every file on 1/1/2025?

No. Given that, we can pick whichever path is easier.

@dowusu-antwi

Copy link
Copy Markdown
Contributor

@natibek do you want someone to take this over?

@natibek

natibek commented Dec 2, 2025

Copy link
Copy Markdown
Contributor Author

@dowusu-antwi, I completely forgot about finalizing to merge this before leaving. Yes, please. It shouldn't require much work. Only need to decide on whether the license check runs as part of _all.py or not.

Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
@bharat-thotakura bharat-thotakura changed the title License header formatting Add licensepy (license header formatting) to checks-superstaq Dec 4, 2025
@bharat-thotakura

bharat-thotakura commented Dec 5, 2025

Copy link
Copy Markdown
Contributor

Pushed some updates to make it ready to merge. In particular:

  1. Added a dedicated license check to the GitHub CI.
  2. Made running the license checker in checks/all_.py optional. This is to not raise any errors for existing users of checks-superstaq's all_.py (e.g., qLDPC) and require running licensepy before opting-in (via the --licensepy) flag. However, we can make this the default behavior if it's deemed as important as all the other checks that are enabled by default in all_.py.
  3. Unpinned a fixed version of licensepy in checks-superstaq/requirements.txt for consistency with the other requirements.
  4. Added an extra and not parsed_args.fix in:
    if return_code > 0 and not parsed_args.fix:
    command = "./checks/licenses.py --fix"

    as the warning message was being printed before applying the fix and also after (both returned a code of 1, hence why)

Something else I noticed: with our transition to ruff, it seems the format and lint check are picking up on trailing whitespaces added in the newlines of the license header upon running ./checks/licenses.py --fix. This still seems to be the case if the template is changed to:

[tool.licensepy]
license_header_template = """Copyright {year} {licensee}\n\nLicensed under the Apache License, Version 2.0 (the "License");\nyou may not use this file except in compliance with the License.\nYou may obtain a copy of the License at\n\nhttp://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an "AS IS" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License."""

Not sure if this is downstream from licensepy (cc: @natibek) or if we're okay with just running ./checks/format_.py post ./checks/licenses.py --fix

Additionally, it seems like licensepy ignores indentation in the template. For example, the spacing for the URL in:

you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,

is not replicated/deleted upon running ./checks/licenses.py --fix.

@bharat-thotakura bharat-thotakura marked this pull request as ready for review December 5, 2025 00:02

@richrines1 richrines1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few nits/questions but otherwise looks great

Comment thread checks-superstaq/checks_superstaq/license_header_format_.py Outdated
Comment thread .github/workflows/ci.yml Outdated
Comment thread checks-superstaq/checks_superstaq/license_header_.py Outdated
Comment thread checks-superstaq/checks_superstaq/licenses.py
bharat-thotakura and others added 4 commits December 5, 2025 15:16
Co-authored-by: richrines1 <85512171+richrines1@users.noreply.github.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>

add `checks/licenses.py`

Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>

remaining renaming

Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>

revert bad rename

Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
Signed-off-by: Bharath <bharath.thotakura@infleqtion.com>
@bharat-thotakura

Copy link
Copy Markdown
Contributor

Should this be a required check in the CI @richrines1 @vtomole?

@richrines1

Copy link
Copy Markdown
Contributor

Should this be a required check in the CI @richrines1 @vtomole?

i think so unless we have any reason not to

@bharat-thotakura bharat-thotakura merged commit c60c661 into main Dec 6, 2025
21 checks passed
@bharat-thotakura bharat-thotakura deleted the license-check branch December 6, 2025 01:31
@natibek

natibek commented Dec 10, 2025

Copy link
Copy Markdown
Contributor Author

Not sure if this is downstream from licensepy (cc: @natibek) or if we're okay with just running ./checks/format_.py post ./checks/licenses.py --fix

@bharat-thotakura, Thank you for finishing the PR! I double-checked the implementation of the formatting, and it does add a new line after the updated license header in an attempt to consistently create separation between the new header and other content in the file. This was especially important to handle the cases where the wrong header is extracted from part of a larger comment. I'll test the outputs with Ruff and make changes so that it doesn't cause formatting issues.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants