diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 000000000..a5dfadb07 --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,223 @@ +# SPDX-FileCopyrightText: 2019 Free Software Foundation Europe e.V. +# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker +# +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Docker Images - test, build and push + +on: + push: + # Tags will carry the tag's version, e.g. v1.2.3: + # - 1.2.3 + # - 1.2 + # - 1 (not with 0 though) + # - latest" + tags: + - "v*.*.*" + # Main branch will be "dev" + branches: + - main + # TODO: remove as soon as master -> main branch + - master + # On PRs only do tests + pull_request: + +jobs: + # =========================================================================== + # Test Docker images + # =========================================================================== + docker_test: + name: Test the docker images + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + # Dockerfile + - name: Build Dockerfile + run: | + docker build -t reuse . + - name: Run Docker image + run: | + docker run -v "$(pwd):/data" reuse + # Dockerfile-extra + - name: Build Dockerfile-extra + run: | + docker build -t reuse-extra --file Dockerfile-extra . + - name: Run Docker extra image + run: | + docker run -v "$(pwd):/data" reuse-extra + # Dockerfile-debian + - name: Build Dockerfile-debian + run: | + docker build -t reuse-debian --file Dockerfile-debian . + - name: Run Docker debian image + run: | + docker run -v "$(pwd):/data" reuse-debian + + # =========================================================================== + # Build and push Docker images for tagged releases + # =========================================================================== + docker_push_tag: + name: Push Docker image for tags to Docker Hub + runs-on: ubuntu-latest + # Depends on successful Docker build/test + needs: + - docker_test + if: + ${{ github.event_name != 'pull_request' && startsWith(github.ref, + 'refs/tags/v') }} + steps: + - name: Check out the repo + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + # Dockerfile + - name: Default Docker - set metadata + id: meta_default + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} + - name: Default Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_default.outputs.tags }} + labels: ${{ steps.meta_default.outputs.labels }} + + # Dockerfile-extra + - name: Extra Docker - set metadata + id: meta_extra + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} + flavor: | + suffix=-extra,onlatest=true + - name: Extra Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile-extra + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_extra.outputs.tags }} + labels: ${{ steps.meta_extra.outputs.labels }} + + # Dockerfile-debian + - name: Debian Docker - set metadata + id: meta_debian + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=semver,pattern={{major}},enable=${{ !startsWith(github.ref, 'refs/tags/v0.') }} + flavor: | + suffix=-debian,onlatest=true + - name: Debian Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile-debian + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_debian.outputs.tags }} + labels: ${{ steps.meta_debian.outputs.labels }} + + # =========================================================================== + # Build and push Docker images for main branch updated (dev tag) + # =========================================================================== + docker_push_dev: + name: Push Docker image for main branch to Docker Hub + runs-on: ubuntu-latest + # Depends on successful Docker build/test + needs: + - docker_test + # TODO: remove master when we switched to main + if: + ${{ github.event_name != 'pull_request' && ( github.ref == + 'refs/heads/main' || github.ref == 'refs/heads/master' ) }} + steps: + - name: Check out the repo + uses: actions/checkout@v2 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Log in to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + # Dockerfile + - name: Default Docker - set metadata + id: meta_default + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=raw,value=dev + - name: Default Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_default.outputs.tags }} + labels: ${{ steps.meta_default.outputs.labels }} + + # Dockerfile-extra + - name: Extra Docker - set metadata + id: meta_extra + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=raw,value=dev-extra + - name: Extra Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile-extra + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_extra.outputs.tags }} + labels: ${{ steps.meta_extra.outputs.labels }} + + # Dockerfile-debian + - name: Debian Docker - set metadata + id: meta_debian + uses: docker/metadata-action@v3 + with: + images: fsfe/reuse + tags: | + type=raw,value=dev-debian + - name: Debian Docker - build and push + uses: docker/build-push-action@v2 + with: + context: . + file: Dockerfile-debian + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta_debian.outputs.tags }} + labels: ${{ steps.meta_debian.outputs.labels }} diff --git a/.github/workflows/latesttag.py b/.github/workflows/latesttag.py index c0f63d113..a2e0aab04 100644 --- a/.github/workflows/latesttag.py +++ b/.github/workflows/latesttag.py @@ -4,6 +4,7 @@ import json import subprocess +import sys from distutils.version import LooseVersion from urllib.request import urlopen @@ -31,4 +32,4 @@ def main(): if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/.github/workflows/latesttag.yaml b/.github/workflows/latesttag.yaml index fd01159e2..260730c9c 100644 --- a/.github/workflows/latesttag.yaml +++ b/.github/workflows/latesttag.yaml @@ -2,7 +2,7 @@ # # SPDX-License-Identifier: GPL-3.0-or-later -name: Verify latest tag +name: Verify latest version tag on PyPI equals version on head of default branch on: schedule: diff --git a/.github/workflows/license_list_up_to_date.py b/.github/workflows/license_list_up_to_date.py index dc5d786e8..a592cd841 100644 --- a/.github/workflows/license_list_up_to_date.py +++ b/.github/workflows/license_list_up_to_date.py @@ -8,35 +8,50 @@ For convenience, also overwrite the files. """ +import json +import sys +import urllib.request from pathlib import Path -import requests +from packaging import version +API_URL = "https://api.github.com/repos/spdx/license-list-data/tags" URLS = { - "exceptions.json": "https://raw.githubusercontent.com/spdx/license-list-data/master/json/exceptions.json", - "licenses.json": "https://raw.githubusercontent.com/spdx/license-list-data/master/json/licenses.json", + "exceptions.json": "https://raw.githubusercontent.com/spdx/license-list-data/{tag}/json/exceptions.json", + "licenses.json": "https://raw.githubusercontent.com/spdx/license-list-data/{tag}/json/licenses.json", } +def latest_tag(): + with urllib.request.urlopen(API_URL) as response: + contents = response.read().decode("utf-8") + dictionary = json.loads(contents) + tags = [item["name"] for item in dictionary] + sorted_tags = sorted(tags, key=version.parse) + return sorted_tags[-1] + + def main(): result = 0 + + tag = latest_tag() + print(f"spdx-license-list-data latest version is {tag}") + for file_, url in URLS.items(): + url = url.format(tag=tag) path = Path(f"src/reuse/resources/{file_}") - contents = path.read_text() - - response = requests.get(url) - if response.status_code == 200: - if response.text == contents: - print(f"{file_} is up-to-date") - else: - result = 1 - print(f"{file_} is not up-to-date") - path.write_text(response.text) + local_contents = path.read_text() + + with urllib.request.urlopen(url) as response: + remote_contents = response.read().decode("utf-8") + if remote_contents == local_contents: + print(f"{file_} is up-to-date") else: result = 1 - print(f"could not download {file_}") + print(f"{file_} is not up-to-date") + path.write_text(remote_contents) return result if __name__ == "__main__": - main() + sys.exit(main()) diff --git a/.github/workflows/prettier.yaml b/.github/workflows/prettier.yaml new file mode 100644 index 000000000..000eb1dba --- /dev/null +++ b/.github/workflows/prettier.yaml @@ -0,0 +1,17 @@ +# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker +# +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Prettier + +on: [push, pull_request] + +jobs: + prettier: + runs-on: ubuntu-latest + container: node:latest + + steps: + - uses: actions/checkout@v2 + - name: Run prettier + run: npx prettier --check . diff --git a/.github/workflows/pythonpackage.yaml b/.github/workflows/pythonpackage.yaml index 47cb7b632..3388735f7 100644 --- a/.github/workflows/pythonpackage.yaml +++ b/.github/workflows/pythonpackage.yaml @@ -12,11 +12,11 @@ jobs: strategy: max-parallel: 10 matrix: - python-version: ['3.6', '3.7', '3.8', '3.9', '3.10'] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] os: [ubuntu-latest, macos-latest, windows-latest] exclude: - os: macos-latest - python-version: '3.6' + python-version: "3.6" steps: - uses: actions/checkout@v2 @@ -104,27 +104,3 @@ jobs: - name: Create docs with Sphinx run: | make docs - - docker: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Build Dockerfile - run: | - docker build -t reuse . - - name: Run Docker image - run: | - docker run -v "$(pwd):/data" reuse - - name: Build Dockerfile-extra - run: | - docker build -t reuse-extra --file Dockerfile-extra . - - name: Run Docker extra image - run: | - docker run -v "$(pwd):/data" reuse-extra - - name: Build Dockerfile-debian - run: | - docker build -t reuse-debian --file Dockerfile-debian . - - name: Run Docker debian image - run: | - docker run -v "$(pwd):/data" reuse-debian diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e227bd043..d914c43d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,6 +19,11 @@ repos: - id: isort name: isort (pyi) types: [pyi] + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.4.1 + hooks: + - id: prettier + name: prettier - repo: local hooks: - id: pylint diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..6c97f861b --- /dev/null +++ b/.prettierignore @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker +# +# SPDX-License-Identifier: GPL-3.0-or-later + +# SPDX resources +/src/reuse/resources/*.json + +# Additional paths in which third-party code/files are located +.env +.pytest_cache/ +.tox/ +.venv +build/ +dist/ +docs/_build/ +env.bak/ +env/ +ENV/ +venv.bak/ +venv/ diff --git a/.prettierrc.yaml b/.prettierrc.yaml new file mode 100644 index 000000000..a770a5368 --- /dev/null +++ b/.prettierrc.yaml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2022 Carmen Bianca Bakker +# +# SPDX-License-Identifier: GPL-3.0-or-later + +printWidth: 80 +proseWrap: always +useTabs: false +tabWidth: 4 + +overrides: + # Identical to file types specified in .editorconfig + - files: + - "*.rst" + - "*.md" + - "*.yaml" + - "*.yml" + - "*.json" + options: + tabWidth: 2 diff --git a/AUTHORS.rst b/AUTHORS.rst index 7d709be9e..0f70e29b4 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -46,6 +46,7 @@ Contributors - Tuomas Siipola +- Gri-ffin Translators ----------- diff --git a/CHANGELOG.md b/CHANGELOG.md index 41f7368ed..66c0d6dab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,14 +40,29 @@ The versions follow [semantic versioning](https://semver.org). ### Added - Recommendations for installation/run methods: package managers and pipx (#457) +- Docker images for AArch64 (#478) + +- More file types are recognised: + + - sbt build files (`.sbt`) + +- Added `--skip-existing` flag to `addheader` in order to skip files that + already contain SPDX information. This may be useful for only adding SPDX + information to newly created files. ### Changed - Use `setuptools` instead of the deprecated `distutils` which will be removed with Python 3.12 (#451) +- `addheader --explicit-license` renamed to `--force-dot-license`. (#476) + ### Deprecated +- Deprecated `--explicit-license` in favour of `--force-dot-license`. + `--explicit-license` will remain useable (although undocumented) for the + foreseeable future. (#476) + ### Removed ### Fixed @@ -57,6 +72,11 @@ The versions follow [semantic versioning](https://semver.org). missing, bad, or unused if `LICENSES/Apache-1.0.txt` exists. It is, however, identified separately as a used license. (#123) +- When `addheader` creates a `.license` file, that file now has a newline at the + end. (#477) + +- Cleaned up internal string manipulation. (#477) + ### Security ## 0.14.0 - 2021-12-27 @@ -137,7 +157,8 @@ contributed! - `addheader` ignores case when matching file extensions and names. (#359) -- Provide `latest-debian` as Docker Hub tag, created by `Dockerfile-debian`. (#321) +- Provide `latest-debian` as Docker Hub tag, created by `Dockerfile-debian`. + (#321) - More file types are recognised: @@ -180,7 +201,8 @@ contributed! - `addheader` now preserves line endings. (#308) -- `download` does no longer fail when both `--output` and `--all` are used. (#326) +- `download` does no longer fail when both `--output` and `--all` are used. + (#326) - Catch erroneous SPDX expressions. (#331) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index badae36d6..cc77a2b30 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,27 +7,28 @@ SPDX-License-Identifier: CC-BY-SA-4.0 # Contribution Guidelines ## Table of Contents -* [Release a new version](#release-a-new-version) + +- [Release a new version](#release-a-new-version) ## Release a new version -* Verify changelog -* Create branch release-0.XX -* `bumpversion --new-version 0.XX.0 minor` -* Alter changelog -* Do some final tweaks/bugfixes (and alter changelog) -* `make update-resources` (and alter changelog again) -* Once everything is good, `git tag -s v0.XX.0` -* `make test-release` -* Test here possibly -* `git tag -d latest` -* `git tag latest` -* `git push --force --tags origin` -* `git push --force --tags upstream` -* `make release` (use one of the documented keys of maintainers) -* `git checkout master` -* `git merge release-0.XX` -* `git push upstream master` -* Update readthedocs (if not happened automatically) -* Update API worker: https://git.fsfe.org/reuse/api-worker#user-content-server -* Make sure package is updated in distros (contact maintainers) +- Verify changelog +- Create branch release-0.XX +- `bumpversion --new-version 0.XX.0 minor` +- Alter changelog +- Do some final tweaks/bugfixes (and alter changelog) +- `make update-resources` (and alter changelog again) +- Once everything is good, `git tag -s v0.XX.0` +- `make test-release` +- Test here possibly +- `git tag -d latest` +- `git tag latest` +- `git push --force --tags origin` +- `git push --force --tags upstream` +- `make release` (use one of the documented keys of maintainers) +- `git checkout master` +- `git merge release-0.XX` +- `git push upstream master` +- Update readthedocs (if not happened automatically) +- Update API worker: https://git.fsfe.org/reuse/api-worker#user-content-server +- Make sure package is updated in distros (contact maintainers) diff --git a/Makefile b/Makefile index 6bf2d962f..b375501df 100644 --- a/Makefile +++ b/Makefile @@ -57,6 +57,10 @@ black: ## format with black isort src/ tests/ *.py black . +.PHONY: prettier +prettier: ## format with prettier + prettier --write . + .PHONY: reuse reuse: dist ## check with self reuse lint diff --git a/README.md b/README.md index 691e05e42..0779e06fd 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ welcome to help us package this tool for more distributions! - Debian: [reuse](https://packages.debian.org/search?keywords=reuse&exact=1) - GNU Guix: [reuse](https://guix.gnu.org/packages/reuse-0.13.0/) - Fedora: [reuse](https://apps.fedoraproject.org/packages/reuse) -- NixOS: [reuse](https://search.nixos.org/packages?channel=21.05&from=0&size=50&sort=relevance&type=packages&query=reuse) +- NixOS: + [reuse](https://search.nixos.org/packages?channel=21.05&from=0&size=50&sort=relevance&type=packages&query=reuse) - openSUSE: [reuse](https://software.opensuse.org/package/reuse) - VoidLinux: [reuse](https://voidlinux.org/packages/?arch=x86_64&q=reuse) diff --git a/src/reuse/_comment.py b/src/reuse/_comment.py index b2c6ce0ce..ed46f4c02 100644 --- a/src/reuse/_comment.py +++ b/src/reuse/_comment.py @@ -57,7 +57,6 @@ def create_comment(cls, text: str, force_multi: bool = False) -> str: :raises CommentCreateError: if *text* could not be commented. """ - text = text.strip("\n") if force_multi or not cls.can_handle_single(): return cls._create_comment_multi(text) return cls._create_comment_single(text) @@ -72,9 +71,8 @@ def _create_comment_single(cls, text: str) -> str: raise CommentCreateError( f"{cls} cannot create single-line comments" ) - text = text.strip("\n") result = [] - for line in text.splitlines(): + for line in text.split("\n"): line_result = cls.SINGLE_LINE if line: line_result += cls.INDENT_AFTER_SINGLE + line @@ -91,10 +89,9 @@ def _create_comment_multi(cls, text: str) -> str: raise CommentCreateError( f"{cls} cannot create multi-line comments" ) - text = text.strip("\n") result = [] result.append(cls.MULTI_LINE[0]) - for line in text.splitlines(): + for line in text.split("\n"): if cls.MULTI_LINE[2] in text: raise CommentCreateError( f"'{line}' contains a premature comment delimiter" @@ -114,8 +111,6 @@ def parse_comment(cls, text: str) -> str: :raises CommentParseError: if *text* could not be parsed. """ - text = text.strip("\n") - try: return cls._parse_comment_single(text) except CommentParseError: @@ -130,7 +125,6 @@ def _parse_comment_single(cls, text: str) -> str: """ if not cls.can_handle_single(): raise CommentParseError(f"{cls} cannot parse single-line comments") - text = text.strip("\n") result = [] for line in text.splitlines(): @@ -143,6 +137,23 @@ def _parse_comment_single(cls, text: str) -> str: result = "\n".join(result) return dedent(result) + @classmethod + def _remove_middle_marker(cls, line: str) -> str: + if cls.MULTI_LINE[1]: + possible_line = line.lstrip() + prefix = cls.MULTI_LINE[1] + if possible_line.startswith(prefix): + line = possible_line.lstrip(prefix) + # Note to future self: line.removeprefix would be preferable + # here. + if line.startswith(cls.INDENT_AFTER_MIDDLE): + line = line.replace(cls.INDENT_AFTER_MIDDLE, "", 1) + else: + _LOGGER.debug( + "'%s' does not contain a middle comment marker", line + ) + return line + @classmethod def _parse_comment_multi(cls, text: str) -> str: """Uncomment all lines in *text*, assuming they are commented by @@ -153,7 +164,6 @@ def _parse_comment_multi(cls, text: str) -> str: if not cls.can_handle_multi(): raise CommentParseError(f"{cls} cannot parse multi-line comments") - text = text.strip("\n") result = [] try: first, *lines, last = text.splitlines() @@ -169,18 +179,10 @@ def _parse_comment_multi(cls, text: str) -> str: f"'{first}' does not start with a comment marker" ) first = first.lstrip(cls.MULTI_LINE[0]) - first = first.strip() + first = first.lstrip() for line in lines: - if cls.MULTI_LINE[1]: - possible_line = line.lstrip(cls.INDENT_BEFORE_MIDDLE) - prefix = cls.MULTI_LINE[1] - if possible_line.startswith(prefix): - line = possible_line.lstrip(prefix) - else: - _LOGGER.debug( - "'%s' does not contain a middle comment marker", line - ) + line = cls._remove_middle_marker(line) result.append(line) if last_is_first: @@ -191,21 +193,13 @@ def _parse_comment_multi(cls, text: str) -> str: f"'{last}' does not end with a comment delimiter" ) last = last.rstrip(cls.MULTI_LINE[2]) - last = last.rstrip(cls.INDENT_BEFORE_END) - last = last.strip() - if cls.MULTI_LINE[1] and last.startswith(cls.MULTI_LINE[1]): - last = last.lstrip(cls.MULTI_LINE[1]) - last = last.lstrip() + last = last.rstrip() + last = cls._remove_middle_marker(last) result = "\n".join(result) result = dedent(result) - if result: - result = "\n".join((first, result, last)) - else: - result = "\n".join((first, last)) - result = result.strip("\n") - return result + return "\n".join(item for item in (first, result, last) if item) @classmethod def comment_at_first_character(cls, text: str) -> str: @@ -308,11 +302,11 @@ class EmptyCommentStyle(CommentStyle): @classmethod def create_comment(cls, text: str, force_multi: bool = False) -> str: - return text.strip("\n") + return text @classmethod def parse_comment(cls, text: str) -> str: - return text.strip("\n") + return text @classmethod def comment_at_first_character(cls, text: str) -> str: @@ -602,6 +596,7 @@ class UncommentableCommentStyle(EmptyCommentStyle): ".rss": HtmlCommentStyle, ".rst": ReStructedTextCommentStyle, ".sass": CssCommentStyle, + ".sbt": CCommentStyle, ".sc": CCommentStyle, # SuperCollider source file ".scad": CCommentStyle, ".scala": PythonCommentStyle, diff --git a/src/reuse/header.py b/src/reuse/header.py index 8c9694c13..e10753d4d 100644 --- a/src/reuse/header.py +++ b/src/reuse/header.py @@ -13,6 +13,7 @@ # pylint: disable=too-many-arguments +import argparse import datetime import logging import os @@ -22,7 +23,7 @@ from gettext import gettext as _ from os import PathLike from pathlib import Path -from typing import Iterable, List, NamedTuple, Optional, Sequence +from typing import Iterable, List, NamedTuple, Optional, Sequence, Tuple from binaryornot.check import is_binary from boolean.boolean import ParseError @@ -100,10 +101,10 @@ def _create_new_header( rendered = template.render( copyright_lines=sorted(spdx_info.copyright_lines), spdx_expressions=sorted(map(str, spdx_info.spdx_expressions)), - ) + ).strip("\n") if template_is_commented: - result = rendered.strip("\n") + result = rendered else: result = style.create_comment(rendered, force_multi=force_multi).strip( "\n" @@ -173,7 +174,7 @@ def create_header( style=style, force_multi=force_multi, ) - return new_header + "\n" + return new_header def _indices_of_newlines(text: str) -> Sequence[int]: @@ -217,6 +218,21 @@ def _find_first_spdx_comment( raise MissingSpdxInfo() +def _extract_shebang(prefix: str, text: str) -> Tuple[str, str]: + """Remove all lines that start with the shebang prefix from *text*. Return a + tuple of (shebang, reduced_text). + """ + shebang_lines = [] + for line in text.splitlines(): + if line.startswith(prefix): + shebang_lines.append(line) + text = text.replace(line, "", 1) + else: + shebang = "\n".join(shebang_lines) + break + return (shebang, text) + + def find_and_replace_header( text: str, spdx_info: SpdxInfo, @@ -264,29 +280,20 @@ def find_and_replace_header( # Keep special first-line-of-file lines as the first line in the file, # or say, move our comments after it. - for (com_style, prefix) in [ - (PythonCommentStyle, "#!"), - (HtmlCommentStyle, " int: """Helper function.""" @@ -414,6 +422,17 @@ def _add_header_to_file( with path.open("r", encoding="utf-8", newline="") as fp: text = fp.read() + # Ideally, this check is done elsewhere. But that would necessitate reading + # the file contents before this function is called. + if skip_existing and contains_spdx_info(text): + out.write( + _( + "Skipped file '{path}' already containing SPDX information" + ).format(path=path) + ) + out.write("\n") + return result + # Detect and remember line endings for later conversion. line_ending = detect_line_endings(text) # Normalise line endings. @@ -454,6 +473,16 @@ def _add_header_to_file( return result +def _verify_write_access(paths: Iterable[PathLike], parser: ArgumentParser): + not_writeable = [ + str(path) for path in paths if not os.access(path, os.W_OK) + ] + if not_writeable: + parser.error( + _("can't write to '{}'").format("', '".join(not_writeable)) + ) + + def add_arguments(parser) -> None: """Add arguments to parser.""" parser.add_argument( @@ -516,13 +545,23 @@ def add_arguments(parser) -> None: parser.add_argument( "--explicit-license", action="store_true", - help=_("place header in path.license instead of path"), + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--force-dot-license", + action="store_true", + help=_("write a .license file instead of a header inside the file"), ) parser.add_argument( "--skip-unrecognised", action="store_true", help=_("skip files with unrecognised comment styles"), ) + parser.add_argument( + "--skip-existing", + action="store_true", + help=_("skip files that already contain SPDX information"), + ) parser.add_argument("path", action="store", nargs="+", type=PathType("r")) @@ -549,14 +588,22 @@ def run(args, project: Project, out=sys.stdout) -> int: " --style" ) ) + if args.explicit_license: + _LOGGER.warning( + _( + "--explicit-license has been deprecated in favour of" + " --force-dot-license" + ) + ) + args.force_dot_license = True paths = [_determine_license_path(path) for path in args.path] - if not args.explicit_license: + if not args.force_dot_license: _verify_write_access(paths, args.parser) # Verify line handling and comment styles before proceeding - if args.style is None and not args.explicit_license: + if args.style is None and not args.force_dot_license: _verify_paths_line_handling( paths, args.parser, @@ -606,7 +653,7 @@ def run(args, project: Project, out=sys.stdout) -> int: result = 0 for path in paths: uncommentable = _is_uncommentable(path) - if uncommentable or args.explicit_license: + if uncommentable or args.force_dot_license: new_path = _determine_license_suffix_path(path) if uncommentable: _LOGGER.info( @@ -618,23 +665,14 @@ def run(args, project: Project, out=sys.stdout) -> int: path = Path(new_path) path.touch() result += _add_header_to_file( - path, - spdx_info, - template, - commented, - args.style, - args.multi_line, - out, + path=path, + spdx_info=spdx_info, + template=template, + template_is_commented=commented, + style=args.style, + force_multi=args.multi_line, + skip_existing=args.skip_existing, + out=out, ) return min(result, 1) - - -def _verify_write_access(paths: Iterable[PathLike], parser: ArgumentParser): - not_writeable = [ - str(path) for path in paths if not os.access(path, os.W_OK) - ] - if not_writeable: - parser.error( - _("can't write to '{}'").format("', '".join(not_writeable)) - ) diff --git a/tests/conftest.py b/tests/conftest.py index 44783fba2..90ed8c698 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -104,12 +104,12 @@ def fake_repository(tmpdir_factory) -> Path: # Adding this here to avoid conflict in main project. (directory / "src/exception.py").write_text( - "SPDX-FileCopyrightText: 2017 Mary Sue\n" + "SPDX-FileCopyrightText: 2017 Jane Doe\n" "SPDX" "-License-Identifier: GPL-3.0-or-later WITH Autoconf-exception-3.0" ) (directory / "src/custom.py").write_text( - "SPDX-FileCopyrightText: 2017 Mary Sue\n" + "SPDX-FileCopyrightText: 2017 Jane Doe\n" "SPDX" "-License-Identifier: LicenseRef-custom" ) @@ -132,7 +132,7 @@ def _repo_contents( "# SPDX" "-License-Identifier: CC0-1.0\n" "# SPDX" - "-FileCopyrightText: 2017 Mary Sue\n" + "-FileCopyrightText: 2017 Jane Doe\n" "*.pyc\nbuild" ) (fake_repository / ignore_filename).write_text(gitignore) diff --git a/tests/resources/fake_repository/.reuse/dep5 b/tests/resources/fake_repository/.reuse/dep5 index 914500a96..e4ffd822e 100644 --- a/tests/resources/fake_repository/.reuse/dep5 +++ b/tests/resources/fake_repository/.reuse/dep5 @@ -1,8 +1,8 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Some project -Upstream-Contact: Mary Sue +Upstream-Contact: Jane Doe Source: https://example.com/ Files: doc/* -Copyright: 2017 Mary Sue +Copyright: 2017 Jane Doe License: CC0-1.0 diff --git a/tests/resources/fake_repository/src/source_code.c b/tests/resources/fake_repository/src/source_code.c index 12cf9fa92..4383c546d 100644 --- a/tests/resources/fake_repository/src/source_code.c +++ b/tests/resources/fake_repository/src/source_code.c @@ -1,4 +1,4 @@ -/* SPDX-FileCopyrightText: 2017 Mary Sue */ +/* SPDX-FileCopyrightText: 2017 Jane Doe */ /* SPDX-License-Identifier: GPL-3.0-or-later */ diff --git a/tests/resources/fake_repository/src/source_code.html b/tests/resources/fake_repository/src/source_code.html index f0f23a630..a967e7571 100644 --- a/tests/resources/fake_repository/src/source_code.html +++ b/tests/resources/fake_repository/src/source_code.html @@ -1,4 +1,4 @@ - +

Hello, world!

diff --git a/tests/resources/fake_repository/src/source_code.jinja2 b/tests/resources/fake_repository/src/source_code.jinja2 index 94ddbcaec..1997b03b0 100644 --- a/tests/resources/fake_repository/src/source_code.jinja2 +++ b/tests/resources/fake_repository/src/source_code.jinja2 @@ -1,4 +1,4 @@ -{# SPDX-FileCopyrightText: 2017 Mary Sue #} +{# SPDX-FileCopyrightText: 2017 Jane Doe #} {# SPDX-License-Identifier: GPL-3.0-or-later #}

Hello, world!

diff --git a/tests/resources/fake_repository/src/source_code.py b/tests/resources/fake_repository/src/source_code.py index 88e4c66a1..08c8f8017 100644 --- a/tests/resources/fake_repository/src/source_code.py +++ b/tests/resources/fake_repository/src/source_code.py @@ -1,4 +1,4 @@ -# SPDX-FileCopyrightText: 2017 Mary Sue +# SPDX-FileCopyrightText: 2017 Jane Doe # # SPDX-License-Identifier: GPL-3.0-or-later diff --git a/tests/test_comment.py b/tests/test_comment.py index 9ee92de5e..c0ae1fb36 100644 --- a/tests/test_comment.py +++ b/tests/test_comment.py @@ -158,10 +158,16 @@ def foo(): assert PythonCommentStyle.parse_comment(text) == expected -def test_create_comment_python_strip_newlines(): - """Don't include unnecessary newlines in the comment.""" +def test_create_comment_python_dont_strip_newlines(): + """Include newlines in the comment.""" text = "\nhello\n" - expected = "# hello" + expected = cleandoc( + """ + # + # hello + # + """ + ) assert PythonCommentStyle.create_comment(text) == expected @@ -172,8 +178,8 @@ def test_create_comment_python_force_multi(): PythonCommentStyle.create_comment("hello", force_multi=True) -def test_parse_comment_python_strip_newlines(): - """When given a comment, remove newlines before and after before parsing.""" +def test_parse_comment_python_fail_on_newline(): + """If a provided comment does not start with the comment character, fail.""" text = dedent( """ @@ -183,9 +189,8 @@ def test_parse_comment_python_strip_newlines(): """ ) - expected = "\nhello\n" - - assert PythonCommentStyle.parse_comment(text) == expected + with pytest.raises(CommentParseError): + assert PythonCommentStyle.parse_comment(text) def test_parse_comment_python_not_a_comment(): @@ -273,6 +278,45 @@ def test_create_comment_c_multi(): assert CCommentStyle.create_comment(text, force_multi=True) == expected +def test_create_comment_c_multi_empty_newlines(): + """Create a C comment that contains empty lines.""" + text = cleandoc( + """ + Hello + + world + """ + ) + expected = cleandoc( + """ + /* + * Hello + * + * world + */ + """ + ) + + assert CCommentStyle.create_comment(text, force_multi=True) == expected + + +def test_create_comment_c_multi_surrounded_by_newlines(): + """Create a C comment that is surrounded by empty lines.""" + text = "\nHello\nworld\n" + expected = cleandoc( + """ + /* + * + * Hello + * world + * + */ + """ + ) + + assert CCommentStyle.create_comment(text, force_multi=True) == expected + + def test_create_comment_c_multi_contains_ending(): """Raise CommentCreateError when the text contains a comment ending.""" text = cleandoc( @@ -523,6 +567,12 @@ def test_parse_comment_html(): assert HtmlCommentStyle.parse_comment(text) == expected +def test_create_comment_html_single(): + """Creating a single-line HTML comment fails.""" + with pytest.raises(CommentCreateError): + HtmlCommentStyle._create_comment_single("hello") + + def test_parse_comment_html_single_line(): """Parse a single-line HTML comment.""" text = "" @@ -531,12 +581,6 @@ def test_parse_comment_html_single_line(): assert HtmlCommentStyle.parse_comment(text) == expected -def test_create_comment_html_single(): - """Creating a single-line HTML comment fails.""" - with pytest.raises(CommentCreateError): - HtmlCommentStyle._create_comment_single("hello") - - def test_comment_at_first_character_python(): """Find the comment block at the first character.""" text = cleandoc( diff --git a/tests/test_header.py b/tests/test_header.py index 85fb8d98b..7f63604ba 100644 --- a/tests/test_header.py +++ b/tests/test_header.py @@ -24,11 +24,11 @@ def test_create_header_simple(): """Create a super simple header.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) expected = cleandoc( """ - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later """ @@ -40,13 +40,13 @@ def test_create_header_simple(): def test_create_header_template_simple(template_simple): """Create a header with a simple template.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) expected = cleandoc( """ # Hello, world! # - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later """ @@ -60,7 +60,7 @@ def test_create_header_template_simple(template_simple): def test_create_header_template_no_spdx(template_no_spdx): """Create a header with a template that does not have all SPDX info.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) with pytest.raises(MissingSpdxInfo): @@ -70,13 +70,13 @@ def test_create_header_template_no_spdx(template_no_spdx): def test_create_header_template_commented(template_commented): """Create a header with an already-commented template.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) expected = cleandoc( """ # Hello, world! # - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later """ @@ -96,7 +96,7 @@ def test_create_header_template_commented(template_commented): def test_create_header_already_contains_spdx(): """Create a new header from a header that already contains SPDX info.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) existing = cleandoc( """ @@ -107,8 +107,8 @@ def test_create_header_already_contains_spdx(): ).replace("spdx", "SPDX") expected = cleandoc( """ + # spdx-FileCopyrightText: Jane Doe # spdx-FileCopyrightText: John Doe - # spdx-FileCopyrightText: Mary Sue # # spdx-License-Identifier: GPL-3.0-or-later # spdx-License-Identifier: MIT @@ -121,7 +121,7 @@ def test_create_header_already_contains_spdx(): def test_create_header_existing_is_wrong(): """If the existing header contains errors, raise a CommentCreateError.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) existing = cleandoc( """ @@ -180,12 +180,12 @@ def test_create_header_remove_fluff(): def test_find_and_replace_no_header(): """Given text without header, add a header.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) text = "pass" expected = cleandoc( """ - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -201,7 +201,7 @@ def test_find_and_replace_verbatim(): spdx_info = SpdxInfo(set(), set()) text = cleandoc( """ - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -217,7 +217,7 @@ def test_find_and_replace_newline_before_header(): preceding whitespace. """ spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: John Doe"} ) text = cleandoc( """ @@ -230,7 +230,7 @@ def test_find_and_replace_newline_before_header(): expected = cleandoc( """ # spdx-FileCopyrightText: Jane Doe - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: John Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -244,7 +244,7 @@ def test_find_and_replace_newline_before_header(): def test_find_and_replace_preserve_preceding(): """When the SPDX header is in the middle of the file, keep it there.""" spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: John Doe"} ) text = cleandoc( """ @@ -266,7 +266,7 @@ def foo(bar): return bar # spdx-FileCopyrightText: Jane Doe - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: John Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -282,7 +282,7 @@ def test_find_and_replace_keep_shebang(): it. """ spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: John Doe"} ) text = cleandoc( """ @@ -298,7 +298,7 @@ def test_find_and_replace_keep_shebang(): #!/usr/bin/env python3 # spdx-FileCopyrightText: Jane Doe - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: John Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -371,7 +371,7 @@ def test_find_and_replace_keep_old_comment(): licensing information, preserve it below the REUSE header. """ spdx_info = SpdxInfo( - {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Mary Sue"} + {"GPL-3.0-or-later"}, {"SPDX" "-FileCopyrightText: Jane Doe"} ) text = cleandoc( """ @@ -382,7 +382,7 @@ def test_find_and_replace_keep_old_comment(): ).replace("spdx", "SPDX") expected = cleandoc( """ - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later @@ -402,7 +402,7 @@ def test_find_and_replace_preserve_newline(): text = ( cleandoc( """ - # spdx-FileCopyrightText: Mary Sue + # spdx-FileCopyrightText: Jane Doe # # spdx-License-Identifier: GPL-3.0-or-later diff --git a/tests/test_main_addheader.py b/tests/test_main_addheader.py index fac945caf..30ed9dbfb 100644 --- a/tests/test_main_addheader.py +++ b/tests/test_main_addheader.py @@ -6,7 +6,7 @@ """Tests for reuse._main: addheader""" -# pylint: disable=unused-argument +# pylint: disable=too-many-lines,unused-argument import stat from inspect import cleandoc @@ -21,6 +21,15 @@ def test_addheader_simple(fake_repository, stringio, mock_date_today): """Add a header to a file that does not have one.""" simple_file = fake_repository / "foo.py" simple_file.write_text("pass") + expected = cleandoc( + """ + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -28,31 +37,29 @@ def test_addheader_simple(fake_repository, stringio, mock_date_today): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.py", ], out=stringio, ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_year(fake_repository, stringio): """Add a header to a file with a custom year.""" simple_file = fake_repository / "foo.py" simple_file.write_text("pass") + expected = cleandoc( + """ + # spdx-FileCopyrightText: 2016 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -62,31 +69,29 @@ def test_addheader_year(fake_repository, stringio): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.py", ], out=stringio, ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # spdx-FileCopyrightText: 2016 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_no_year(fake_repository, stringio): """Add a header to a file without a year.""" simple_file = fake_repository / "foo.py" simple_file.write_text("pass") + expected = cleandoc( + """ + # spdx-FileCopyrightText: Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -95,31 +100,29 @@ def test_addheader_no_year(fake_repository, stringio): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.py", ], out=stringio, ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # spdx-FileCopyrightText: Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_specify_style(fake_repository, stringio, mock_date_today): """Add a header to a file that does not have one, using a custom style.""" simple_file = fake_repository / "foo.py" simple_file.write_text("pass") + expected = cleandoc( + """ + // spdx-FileCopyrightText: 2018 Jane Doe + // + // spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -127,7 +130,7 @@ def test_addheader_specify_style(fake_repository, stringio, mock_date_today): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--style", "c", "foo.py", @@ -136,24 +139,22 @@ def test_addheader_specify_style(fake_repository, stringio, mock_date_today): ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - // spdx-FileCopyrightText: 2018 Mary Sue - // - // spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_implicit_style(fake_repository, stringio, mock_date_today): """Add a header to a file that has a recognised extension.""" simple_file = fake_repository / "foo.js" simple_file.write_text("pass") + expected = cleandoc( + """ + // spdx-FileCopyrightText: 2018 Jane Doe + // + // spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -161,25 +162,14 @@ def test_addheader_implicit_style(fake_repository, stringio, mock_date_today): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.js", ], out=stringio, ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - // spdx-FileCopyrightText: 2018 Mary Sue - // - // spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_implicit_style_filename( @@ -188,6 +178,15 @@ def test_addheader_implicit_style_filename( """Add a header to a filename that is recognised.""" simple_file = fake_repository / "Makefile" simple_file.write_text("pass") + expected = cleandoc( + """ + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -195,25 +194,14 @@ def test_addheader_implicit_style_filename( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "Makefile", ], out=stringio, ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_unrecognised_style(fake_repository): @@ -228,7 +216,7 @@ def test_addheader_unrecognised_style(fake_repository): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.foo", ] ) @@ -245,7 +233,7 @@ def test_addheader_skip_unrecognised(fake_repository, stringio): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--skip-unrecognised", "foo.foo", ], @@ -269,7 +257,7 @@ def test_addheader_skip_unrecognised_and_style( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--style=c", "--skip-unrecognised", "foo.foo", @@ -299,6 +287,17 @@ def test_addheader_template_simple( template_file = fake_repository / ".reuse/templates/mytemplate.jinja2" template_file.parent.mkdir(parents=True, exist_ok=True) template_file.write_text(template_simple_source) + expected = cleandoc( + """ + # Hello, world! + # + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -306,7 +305,7 @@ def test_addheader_template_simple( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate.jinja2", "foo.py", @@ -315,20 +314,7 @@ def test_addheader_template_simple( ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # Hello, world! - # - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_template_simple_multiple( @@ -348,7 +334,7 @@ def test_addheader_template_simple_multiple( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate.jinja2", ] @@ -358,20 +344,18 @@ def test_addheader_template_simple_multiple( assert result == 0 for simple_file in simple_files: - assert ( - simple_file.read_text() - == cleandoc( - """ - # Hello, world! - # - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later + expected = cleandoc( + """ + # Hello, world! + # + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later - pass - """ - ).replace("spdx", "SPDX") - ) + pass + """ + ).replace("spdx", "SPDX") + assert simple_file.read_text() == expected def test_addheader_template_no_spdx( @@ -390,7 +374,7 @@ def test_addheader_template_no_spdx( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate.jinja2", "foo.py", @@ -412,6 +396,17 @@ def test_addheader_template_commented( ) template_file.parent.mkdir(parents=True, exist_ok=True) template_file.write_text(template_commented_source) + expected = cleandoc( + """ + # Hello, world! + # + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -419,7 +414,7 @@ def test_addheader_template_commented( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate.commented.jinja2", "foo.c", @@ -428,20 +423,7 @@ def test_addheader_template_commented( ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # Hello, world! - # - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_template_nonexistant(fake_repository): @@ -457,7 +439,7 @@ def test_addheader_template_nonexistant(fake_repository): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate.jinja2", "foo.py", @@ -475,6 +457,17 @@ def test_addheader_template_without_extension( template_file = fake_repository / ".reuse/templates/mytemplate.jinja2" template_file.parent.mkdir(parents=True, exist_ok=True) template_file.write_text(template_simple_source) + expected = cleandoc( + """ + # Hello, world! + # + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") result = main( [ @@ -482,7 +475,7 @@ def test_addheader_template_without_extension( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--template", "mytemplate", "foo.py", @@ -491,20 +484,7 @@ def test_addheader_template_without_extension( ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - # Hello, world! - # - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later - - pass - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected def test_addheader_binary( @@ -513,6 +493,13 @@ def test_addheader_binary( """Add a header to a .license file if the file is a binary.""" binary_file = fake_repository / "foo.png" binary_file.write_bytes(binary_string) + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -520,7 +507,7 @@ def test_addheader_binary( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.png", ], out=stringio, @@ -531,13 +518,7 @@ def test_addheader_binary( binary_file.with_name(f"{binary_file.name}.license") .read_text() .strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue - - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") + == expected ) @@ -547,6 +528,13 @@ def test_addheader_uncommentable_json( """Add a header to a .license file if the file is uncommentable, e.g., JSON.""" json_file = fake_repository / "foo.json" json_file.write_text('{"foo": 23, "bar": 42}') + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -554,7 +542,7 @@ def test_addheader_uncommentable_json( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.json", ], out=stringio, @@ -563,22 +551,23 @@ def test_addheader_uncommentable_json( assert result == 0 assert ( json_file.with_name(f"{json_file.name}.license").read_text().strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue - - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") + == expected ) -def test_addheader_explicit_license( +def test_addheader_force_dot_license( fake_repository, stringio, mock_date_today ): - """Add a header to a .license file if --explicit-license is given.""" + """Add a header to a .license file if --force-dot-license is given.""" simple_file = fake_repository / "foo.py" simple_file.write_text("pass") + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -586,8 +575,8 @@ def test_addheader_explicit_license( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", - "--explicit-license", + "Jane Doe", + "--force-dot-license", "foo.py", ], out=stringio, @@ -598,18 +587,54 @@ def test_addheader_explicit_license( simple_file.with_name(f"{simple_file.name}.license") .read_text() .strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue - - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") + == expected ) assert simple_file.read_text() == "pass" -def test_addheader_explicit_license_double( +def test_addheader_force_dot_license_identical_to_explicit_license( + fake_repository, stringio, mock_date_today +): + """For backwards compatibility, --force-dot-license should have identical + results as --explicit-license. + """ + files = [ + fake_repository / "foo.py", + fake_repository / "bar.py", + ] + for path in files: + path.write_text("pass") + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") + + for arg, path in zip(("--force-dot-license", "--explicit-license"), files): + main( + [ + "addheader", + "--license", + "GPL-3.0-or-later", + "--copyright", + "Jane Doe", + arg, + str(path), + ], + out=stringio, + ) + + for path in files: + assert ( + path.with_name(f"{path.name}.license").read_text().strip() + == expected + ) + assert path.read_text() == "pass" + + +def test_addheader_force_dot_license_double( fake_repository, stringio, mock_date_today ): """When path.license already exists, don't create path.license.license.""" @@ -619,6 +644,13 @@ def test_addheader_explicit_license_double( simple_file.write_text("foo") simple_file_license.write_text("foo") + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -626,8 +658,8 @@ def test_addheader_explicit_license_double( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", - "--explicit-license", + "Jane Doe", + "--force-dot-license", "foo.txt", ], out=stringio, @@ -635,26 +667,24 @@ def test_addheader_explicit_license_double( assert result == 0 assert not simple_file_license_license.exists() - assert ( - simple_file_license.read_text().strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue + assert simple_file_license.read_text().strip() == expected - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") - ) - -def test_addheader_explicit_license_unsupported_filetype( +def test_addheader_force_dot_license_unsupported_filetype( fake_repository, stringio, mock_date_today ): - """Add a header to a .license file if --explicit-license is given, with the + """Add a header to a .license file if --force-dot-license is given, with the base file being an otherwise unsupported filetype. """ simple_file = fake_repository / "foo.txt" simple_file.write_text("Preserve this") + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -662,8 +692,8 @@ def test_addheader_explicit_license_unsupported_filetype( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", - "--explicit-license", + "Jane Doe", + "--force-dot-license", "foo.txt", ], out=stringio, @@ -674,26 +704,27 @@ def test_addheader_explicit_license_unsupported_filetype( simple_file.with_name(f"{simple_file.name}.license") .read_text() .strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue - - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") + == expected ) assert simple_file.read_text() == "Preserve this" -def test_addheader_explicit_license_doesnt_write_to_file( +def test_addheader_force_dot_license_doesnt_write_to_file( fake_repository, stringio, mock_date_today ): - """Adding a header to a .license file if --explicit-license is given, + """Adding a header to a .license file if --force-dot-license is given, doesn't require write permission to the file, just the directory. """ simple_file = fake_repository / "foo.txt" simple_file.write_text("Preserve this") simple_file.chmod(mode=stat.S_IREAD) + expected = cleandoc( + """ + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") result = main( [ @@ -701,8 +732,8 @@ def test_addheader_explicit_license_doesnt_write_to_file( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", - "--explicit-license", + "Jane Doe", + "--force-dot-license", "foo.txt", ], out=stringio, @@ -713,13 +744,7 @@ def test_addheader_explicit_license_doesnt_write_to_file( simple_file.with_name(f"{simple_file.name}.license") .read_text() .strip() - == cleandoc( - """ - spdx-FileCopyrightText: 2018 Mary Sue - - spdx-License-Identifier: GPL-3.0-or-later - """ - ).replace("spdx", "SPDX") + == expected ) assert simple_file.read_text() == "Preserve this" @@ -756,12 +781,23 @@ def test_addheader_license_file(fake_repository, stringio, mock_date_today): license_file.write_text( cleandoc( """ - spdx-FileCopyrightText: 2016 Jane Doe + spdx-FileCopyrightText: 2016 John Doe Hello """ ).replace("spdx", "SPDX") ) + expected = ( + cleandoc( + """ + spdx-FileCopyrightText: 2016 John Doe + spdx-FileCopyrightText: 2018 Jane Doe + + spdx-License-Identifier: GPL-3.0-or-later + """ + ).replace("spdx", "SPDX") + + "\n" + ) result = main( [ @@ -769,24 +805,60 @@ def test_addheader_license_file(fake_repository, stringio, mock_date_today): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.py", ], out=stringio, ) assert result == 0 - assert ( - license_file.read_text() - == cleandoc( + assert license_file.read_text() == expected + assert simple_file.read_text() == "foo" + + +def test_addheader_license_file_only_one_newline( + fake_repository, stringio, mock_date_today +): + """When a header is added to a .license file that already ends with a newline, the new header should end with a single newline.""" + simple_file = fake_repository / "foo.py" + simple_file.write_text("foo") + license_file = fake_repository / "foo.py.license" + license_file.write_text( + cleandoc( """ - spdx-FileCopyrightText: 2016 Jane Doe - spdx-FileCopyrightText: 2018 Mary Sue + spdx-FileCopyrightText: 2016 John Doe + + Hello + """ + ).replace("spdx", "SPDX") + + "\n" + ) + expected = ( + cleandoc( + """ + spdx-FileCopyrightText: 2016 John Doe + spdx-FileCopyrightText: 2018 Jane Doe spdx-License-Identifier: GPL-3.0-or-later """ ).replace("spdx", "SPDX") + + "\n" ) + + result = main( + [ + "addheader", + "--license", + "GPL-3.0-or-later", + "--copyright", + "Jane Doe", + "foo.py", + ], + out=stringio, + ) + + assert result == 0 + assert license_file.read_text() == expected assert simple_file.read_text() == "foo" @@ -799,7 +871,7 @@ def test_addheader_year_mutually_exclusive(fake_repository): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--exclude-year", "--year", "2020", @@ -817,7 +889,7 @@ def test_addheader_single_multi_line_mutually_exclusive(fake_repository): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--single-line", "--multi-line", "src/source_code.c", @@ -835,7 +907,7 @@ def test_addheader_multi_line_not_supported(fake_repository, skip_option): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--multi-line", skip_option, "src/source_code.py", @@ -853,7 +925,7 @@ def test_addheader_single_line_not_supported(fake_repository, skip_option): "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--single-line", skip_option, "src/source_code.html", @@ -867,6 +939,17 @@ def test_addheader_force_multi_line_for_c( """--multi-line forces a multi-line comment for C.""" simple_file = fake_repository / "foo.c" simple_file.write_text("foo") + expected = cleandoc( + """ + /* + * spdx-FileCopyrightText: 2018 Jane Doe + * + * spdx-License-Identifier: GPL-3.0-or-later + */ + + foo + """ + ).replace("spdx", "SPDX") result = main( [ @@ -874,7 +957,7 @@ def test_addheader_force_multi_line_for_c( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "--multi-line", "foo.c", ], @@ -882,20 +965,7 @@ def test_addheader_force_multi_line_for_c( ) assert result == 0 - assert ( - simple_file.read_text() - == cleandoc( - """ - /* - * spdx-FileCopyrightText: 2018 Mary Sue - * - * spdx-License-Identifier: GPL-3.0-or-later - */ - - foo - """ - ).replace("spdx", "SPDX") - ) + assert simple_file.read_text() == expected @pytest.mark.parametrize("line_ending", ["\r\n", "\r", "\n"]) @@ -907,6 +977,20 @@ def test_addheader_line_endings( simple_file.write_bytes( line_ending.encode("utf-8").join([b"hello", b"world"]) ) + expected = ( + cleandoc( + """ + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + hello + world + """ + ) + .replace("spdx", "SPDX") + .replace("\n", line_ending) + ) result = main( [ @@ -914,7 +998,7 @@ def test_addheader_line_endings( "--license", "GPL-3.0-or-later", "--copyright", - "Mary Sue", + "Jane Doe", "foo.py", ], out=stringio, @@ -924,18 +1008,59 @@ def test_addheader_line_endings( with open(simple_file, newline="") as fp: contents = fp.read() - assert ( - contents - == cleandoc( - """ - # spdx-FileCopyrightText: 2018 Mary Sue - # - # spdx-License-Identifier: GPL-3.0-or-later + assert contents == expected - hello - world - """ - ) - .replace("spdx", "SPDX") - .replace("\n", line_ending) + +def test_addheader_skip_existing(fake_repository, stringio, mock_date_today): + """When addheader --skip-existing on a file that already contains SPDX info, + don't write additional information to it. + """ + for path in ("foo.py", "bar.py"): + (fake_repository / path).write_text("pass") + expected_foo = cleandoc( + """ + # spdx-FileCopyrightText: 2018 Jane Doe + # + # spdx-License-Identifier: GPL-3.0-or-later + + pass + """ + ).replace("spdx", "SPDX") + expected_bar = cleandoc( + """ + # spdx-FileCopyrightText: 2018 John Doe + # + # spdx-License-Identifier: MIT + + pass + """ + ).replace("spdx", "SPDX") + + main( + [ + "addheader", + "--license", + "GPL-3.0-or-later", + "--copyright", + "Jane Doe", + "foo.py", + ], + out=stringio, ) + + result = main( + [ + "addheader", + "--license", + "MIT", + "--copyright", + "John Doe", + "--skip-existing", + "foo.py", + "bar.py", + ] + ) + + assert result == 0 + assert (fake_repository / "foo.py").read_text() == expected_foo + assert (fake_repository / "bar.py").read_text() == expected_bar diff --git a/tests/test_project.py b/tests/test_project.py index ae0b653b0..b2533085f 100644 --- a/tests/test_project.py +++ b/tests/test_project.py @@ -101,7 +101,7 @@ def test_all_files_symlinks(empty_directory): (empty_directory / "blob.license").write_text( cleandoc( """ - spdx-FileCopyrightText: Mary Sue + spdx-FileCopyrightText: Jane Doe spdx-License-Identifier: GPL-3.0-or-later """ @@ -231,7 +231,7 @@ def test_spdx_info_of_only_copyright(fake_repository): up on that. """ (fake_repository / "foo.py").write_text( - "SPDX-FileCopyrightText: 2017 Mary Sue" + "SPDX-FileCopyrightText: 2017 Jane Doe" ) project = Project(fake_repository) spdx_info = project.spdx_info_of("foo.py") @@ -239,7 +239,7 @@ def test_spdx_info_of_only_copyright(fake_repository): assert len(spdx_info.copyright_lines) == 1 assert ( spdx_info.copyright_lines.pop() - == "SPDX-FileCopyrightText: 2017 Mary Sue" + == "SPDX-FileCopyrightText: 2017 Jane Doe" ) @@ -255,7 +255,7 @@ def test_spdx_info_of_only_copyright_also_covered_by_debian(fake_repository): assert any(spdx_info.spdx_expressions) assert len(spdx_info.copyright_lines) == 2 assert "SPDX-FileCopyrightText: in file" in spdx_info.copyright_lines - assert "2017 Mary Sue" in spdx_info.copyright_lines + assert "2017 Jane Doe" in spdx_info.copyright_lines def test_spdx_info_of_also_covered_by_dep5(fake_repository): @@ -274,7 +274,7 @@ def test_spdx_info_of_also_covered_by_dep5(fake_repository): assert LicenseSymbol("MIT") in spdx_info.spdx_expressions assert LicenseSymbol("CC0-1.0") in spdx_info.spdx_expressions assert "SPDX-FileCopyrightText: in file" in spdx_info.copyright_lines - assert "2017 Mary Sue" in spdx_info.copyright_lines + assert "2017 Jane Doe" in spdx_info.copyright_lines def test_spdx_info_of_no_duplicates(empty_directory): @@ -316,13 +316,13 @@ def test_license_file_detected(empty_directory): """ (empty_directory / "foo.py").write_text("foo") (empty_directory / "foo.py.license").write_text( - "SPDX-FileCopyrightText: 2017 Mary Sue\nSPDX-License-Identifier: MIT\n" + "SPDX-FileCopyrightText: 2017 Jane Doe\nSPDX-License-Identifier: MIT\n" ) project = Project(empty_directory) spdx_info = project.spdx_info_of("foo.py") - assert "SPDX-FileCopyrightText: 2017 Mary Sue" in spdx_info.copyright_lines + assert "SPDX-FileCopyrightText: 2017 Jane Doe" in spdx_info.copyright_lines assert LicenseSymbol("MIT") in spdx_info.spdx_expressions diff --git a/tests/test_report.py b/tests/test_report.py index f49369fc6..f9568784f 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -32,7 +32,7 @@ def test_generate_file_report_file_simple(fake_repository): project = Project(fake_repository) result = FileReport.generate(project, "src/source_code.py") assert result.spdxfile.licenses_in_file == ["GPL-3.0-or-later"] - assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Mary Sue" + assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses @@ -45,7 +45,7 @@ def test_generate_file_report_file_from_different_cwd(fake_repository): project, fake_repository / "src/source_code.py" ) assert result.spdxfile.licenses_in_file == ["GPL-3.0-or-later"] - assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Mary Sue" + assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses @@ -100,7 +100,7 @@ def test_generate_file_report_exception(fake_repository): "GPL-3.0-or-later", "Autoconf-exception-3.0", } - assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Mary Sue" + assert result.spdxfile.copyright == "SPDX-FileCopyrightText: 2017 Jane Doe" assert not result.bad_licenses assert not result.missing_licenses diff --git a/tests/test_util.py b/tests/test_util.py index 9c4a5251e..7d99524b6 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -137,7 +137,7 @@ def test_copyright_from_dep5(copyright): """Verify that the glob in the dep5 file is matched.""" result = _util._copyright_from_dep5("doc/foo.rst", copyright) assert LicenseSymbol("CC0-1.0") in result.spdx_expressions - assert "2017 Mary Sue" in result.copyright_lines + assert "2017 Jane Doe" in result.copyright_lines def test_make_copyright_line_simple():